diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt new file mode 100644 index 0000000000..eb62eaf6e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -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) { + 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 + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt new file mode 100644 index 0000000000..83355647bd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -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> { + 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.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.addInformationalNotifications( + cardTypesResolver: CardTypesResolver, + maybeTokenList: Either, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.Informational.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(maybeTokenList, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + maybeTokenList: Either, + 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.getMissingAddressCurrencies(): List { + 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.addWarningNotifications( + cardTypesResolver: CardTypesResolver, + tokenList: Either, + 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.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.addRateTheAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ), + condition = isReadyToShowRating && readyForRateAppNotification, + ) + } + + private fun MutableList.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 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt new file mode 100644 index 0000000000..a248e07dbe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -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> { + 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.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.addInformationalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Informational.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver, + maybePrimaryCurrencyStatus: Either, + 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.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.addRateTheAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ), + condition = isReadyToShowRating && readyForRateAppNotification, + ) + } + + private fun MutableList.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 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt new file mode 100644 index 0000000000..3426a96ae1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -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) { + 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.hasNonZeroWallets(): Boolean { + return any { + val amount = it.value.amount ?: return@any false + !amount.isZero() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt new file mode 100644 index 0000000000..08b89be0c7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -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>(name = "multi_wallet_warnings") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getMultiWalletWarningsFactory.create(clickIntents) + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update( + SetWarningsTransformer(userWalletId = userWalletId, warnings = it), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt new file mode 100644 index 0000000000..fde79c3d02 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -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>(name = "primary_currency") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(::sendAnalyticsEvent) + .onEach(::checkWalletWithFunds) + } + + private fun updateContent(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + stateHolder.update( + SetPrimaryCurrencyTransformer( + status = status.value, + userWallet = userWallet, + appCurrency = appCurrency, + ), + ) + } + + private fun sendAnalyticsEvent(maybeCurrencyStatus: Either) { + 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) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt new file mode 100644 index 0000000000..1f0de740d0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -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(name = "single_wallet_buttons") { + + override fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow { + 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, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt new file mode 100644 index 0000000000..64df0d025e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -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>(name = "single_wallet_warnings") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getSingleWalletWarningsFactory.create(clickIntents) + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update( + SetWarningsTransformer(userWalletId = userWalletId, warnings = it), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt new file mode 100644 index 0000000000..a6446944c0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -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>(name = "single_wallet_with_token_list") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getCardTokensListUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(tokenListAnalyticsSender::send) + .onEach(walletWithFundsChecker::check) + } + + private fun updateContent(maybeTokenList: Either) { + stateHolder.update( + maybeTokenList.fold( + ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) }, + ifRight = { + SetTokenListTransformer( + tokenList = it, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt new file mode 100644 index 0000000000..4c4caf4479 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt @@ -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> + +@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>(name = "token_list") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getTokenListUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(tokenListAnalyticsSender::send) + .onEach(walletWithFundsChecker::check) + } + + private fun updateContent(maybeTokenList: Either) { + stateHolder.update( + maybeTokenList.fold( + ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) }, + ifRight = { + SetTokenListTransformer( + tokenList = it, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt new file mode 100644 index 0000000000..6da06e3029 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -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 +typealias MaybeTxHistoryItems = Either>> + +@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>(name = "tx_history") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + 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, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt new file mode 100644 index 0000000000..112c7cbe8a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt @@ -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(val name: String) { + + protected abstract fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow + + fun subscribe(coroutineScope: CoroutineScope, dispatchers: CoroutineDispatcherProvider): Job { + Timber.d("Subscribe on $name") + return create(coroutineScope, dispatchers.main) + .flowOn(dispatchers.main) + .launchIn(coroutineScope) + } +} \ No newline at end of file