diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 17782946ce..1c596a4b4d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -91,4 +91,16 @@ internal object SettingsDomainModule { ): UpdateBalanceHidingSettingsUseCase { return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository) } + + @Provides + @ViewModelScoped + fun provideSetWalletsScrollPreviewIsShown(settingsRepository: SettingsRepository): NeverToShowWalletsScrollPreview { + return NeverToShowWalletsScrollPreview(settingsRepository = settingsRepository) + } + + @Provides + @ViewModelScoped + fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled { + return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 1ec868ce0e..101c7c51e5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -20,6 +20,8 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* @@ -90,13 +92,17 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { submitNewAdd( - userWalletId = action.userWallet.walletId, + userWallet = action.userWallet, updatedScanResponse = it, currencyList = currencyList, ) } } else { - submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList) + submitNewAdd( + userWallet = action.userWallet, + updatedScanResponse = scanResponse, + currencyList = currencyList, + ) } } } @@ -375,16 +381,18 @@ object TokensMiddleware { } private fun submitNewAdd( - userWalletId: UserWalletId, + userWallet: UserWallet, updatedScanResponse: ScanResponse, currencyList: List, ) { scope.launch { userWalletsListManager.update( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, update = { it.copy(scanResponse = updatedScanResponse) }, ).doOnSuccess { - addCryptoCurrenciesUseCase(userWalletId, currencyList) + addCryptoCurrenciesUseCase(userWallet.walletId, currencyList).onRight { + store.dispatch(action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet)) + } } } store.dispatchOnMain(NavigationAction.PopBackTo()) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 7f43d0a7e8..65be83c11a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -34,6 +34,8 @@ object PreferencesKeys { val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") } + + val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 9da64e96a0..106d635c23 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -34,5 +34,9 @@ { "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "undefined" + }, + { + "name": "WALLETS_SCROLLING_PREVIEW_ENABLED", + "version": "5.4.0" } ] diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt index e6f3ad0d84..516dba38e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.event import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable +import kotlinx.coroutines.launch /** * A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event @@ -17,8 +18,8 @@ import androidx.compose.runtime.NonRestartableComposable fun EventEffect(event: StateEvent, onTrigger: suspend (data: A) -> Unit) { LaunchedEffect(event) { if (event is StateEvent.Triggered) { - onTrigger(event.data) - event.onConsume() + launch { onTrigger(event.data) } + .invokeOnCompletion { event.onConsume() } } } } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index 7f0a56fd9a..84c73a8dc7 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -3,7 +3,7 @@ package com.tangem.data.card import com.tangem.datasource.local.card.UsedCardInfo import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.domain.card.repository.CardRepository import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow @@ -14,7 +14,7 @@ internal class DefaultCardRepository( ) : CardRepository { override fun wasCardScanned(cardId: String): Flow { - return appPreferencesStore.getObject>(key = PreferencesKeys.USED_CARDS_INFO_KEY) + return appPreferencesStore.getObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY) .map { savedCards -> savedCards?.any { it.cardId == cardId } ?: false } @@ -22,14 +22,14 @@ internal class DefaultCardRepository( override suspend fun setCardWasScanned(cardId: String) { appPreferencesStore.editData { mutablePreferences -> - val usedCards: List? = mutablePreferences.getObject( + val usedCards: List? = mutablePreferences.getObjectList( key = PreferencesKeys.USED_CARDS_INFO_KEY, ) val updatedUsedCards = usedCards?.updateCard(cardId) ?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)) - mutablePreferences.setObject( + mutablePreferences.setObjectList( key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards, ) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 0fed0929d9..8d3c6f18bc 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -1,16 +1,35 @@ package com.tangem.data.settings import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultSettingsRepository( private val preferencesDataSource: PreferencesDataSource, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : SettingsRepository { override suspend fun shouldShowSaveUserWalletScreen(): Boolean { return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen } } + + override suspend fun isWalletScrollPreviewEnabled(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY, + default = true, + ) + } + + override suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) { + appPreferencesStore.store( + key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY, + value = isEnabled, + ) + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 254235cf84..ba83b09d4d 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -21,10 +21,12 @@ internal object SettingsDataModule { @Singleton fun provideSettingsRepository( preferencesDataSource: PreferencesDataSource, + appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, ): SettingsRepository { return DefaultSettingsRepository( preferencesDataSource = preferencesDataSource, + appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, ) } diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt new file mode 100644 index 0000000000..fc45e34aa0 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Checks if wallets scroll preview is enabled + * + * @property settingsRepository settings repository + * +[REDACTED_AUTHOR] + */ +class IsWalletsScrollPreviewEnabled(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.isWalletScrollPreviewEnabled() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt new file mode 100644 index 0000000000..4080a08bc9 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Never to show wallets scroll preview + * + * @property settingsRepository settings repository + * +[REDACTED_AUTHOR] + */ +class NeverToShowWalletsScrollPreview( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke() = settingsRepository.setWalletScrollPreviewAvailability(isEnabled = false) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index cdfe2702da..7dd4507554 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -3,4 +3,8 @@ package com.tangem.domain.settings.repositories interface SettingsRepository { suspend fun shouldShowSaveUserWalletScreen(): Boolean + + suspend fun isWalletScrollPreviewEnabled(): Boolean + + suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 2f9b61a098..2277937e9d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -32,10 +32,7 @@ class GetCryptoCurrencyActionsUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - suspend operator fun invoke( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Flow { + operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -43,19 +40,25 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id - val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) - } else if (!userWallet.isMultiCurrency) { - operations.getPrimaryCurrencyStatusFlow() - } else { - operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath) - } - return networkFlow.mapLatest { maybeCoinStatus -> - createTokenActionsState( - userWalletId = userWallet.walletId, - coinStatus = maybeCoinStatus.getOrNull(), - cryptoCurrencyStatus = cryptoCurrencyStatus, - ) + + return flow { + val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + } else if (!userWallet.isMultiCurrency) { + operations.getPrimaryCurrencyStatusFlow() + } else { + operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath) + } + + val flow = networkFlow.mapLatest { maybeCoinStatus -> + createTokenActionsState( + userWalletId = userWallet.walletId, + coinStatus = maybeCoinStatus.getOrNull(), + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + } + + emitAll(flow) }.flowOn(dispatchers.io) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 4c894e93d2..84303de548 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -2,12 +2,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess +import arrow.core.right +import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull import com.tangem.domain.wallets.models.SelectWalletError +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId /** @@ -22,16 +23,20 @@ class SelectWalletUseCase( private val reduxStateHolder: ReduxStateHolder, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either { + suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { val userWalletsListManager = ensureUserWalletListManagerNotNull( walletsStateHolder = walletsStateHolder, raise = { SelectWalletError.DataError }, ) - userWalletsListManager.select(userWalletId) - .doOnFailure { raise(SelectWalletError.UnableToSelectUserWallet) } - .doOnSuccess { reduxStateHolder.onUserWalletSelected(it) } + return when (val result = userWalletsListManager.select(userWalletId)) { + is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) + is CompletionResult.Success -> { + reduxStateHolder.onUserWalletSelected(result.data) + result.data.right() + } + } } } } \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 5af5379a9e..9485b4d30c 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -9,4 +9,6 @@ interface WalletFeatureToggles { /** Availability of redesigned screen */ val isRedesignedScreenEnabled: Boolean + + val isWalletsScrollingPreviewEnabled: Boolean } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 1e64fc4122..81c4fd83ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.di import com.tangem.core.navigation.ReduxNavController import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module import dagger.Provides @@ -15,7 +16,10 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { - return DefaultWalletRouter(reduxNavController = reduxNavController) + fun provideWalletRouter( + reduxNavController: ReduxNavController, + walletFeatureToggles: WalletFeatureToggles, + ): WalletRouter { + return DefaultWalletRouter(reduxNavController = reduxNavController, walletFeatureToggles = walletFeatureToggles) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index 71696ceba1..09655bdd61 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -16,4 +16,7 @@ internal class DefaultWalletFeatureToggles( override val isRedesignedScreenEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_WALLET_SCREEN_ENABLED") + + override val isWalletsScrollingPreviewEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "WALLETS_SCROLLING_PREVIEW_ENABLED") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 646cec18f5..5dd19e4a33 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -26,12 +26,18 @@ import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen +import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreenV2 import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModelV2 import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlin.properties.Delegates /** Default implementation of wallet feature router */ -internal class DefaultWalletRouter(private val reduxNavController: ReduxNavController) : InnerWalletRouter { +internal class DefaultWalletRouter( + private val reduxNavController: ReduxNavController, + private val walletFeatureToggles: WalletFeatureToggles, +) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() private var onFinish: () -> Unit = {} @@ -47,10 +53,19 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr startDestination = WalletRoute.Wallet.route, ) { composable(WalletRoute.Wallet.route) { - val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) { + val viewModel = hiltViewModel().apply { + router = this@DefaultWalletRouter + } - WalletScreen(state = viewModel.uiState) + WalletScreen(state = viewModel.uiState) + } else { + val viewModel = hiltViewModel().apply { + setWalletRouter(router = this@DefaultWalletRouter) + } + + WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value) + } } composable( 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/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt new file mode 100644 index 0000000000..7e62eb79e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +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.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import kotlinx.coroutines.flow.* +import timber.log.Timber + +internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { + return this().fold( + ifLeft = { + Timber.e("Impossible to get selected wallet $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return this(userWalletId) + .conflate() + .distinctUntilChanged() + .filter(Either::isRight) + .firstOrNull() + ?.fold( + ifLeft = { + Timber.e("Impossible to get primary currency status $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetSelectedAppCurrencyUseCase.unwrap(): AppCurrency { + return this() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .firstOrNull() + ?: AppCurrency.Default +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.collectLatest( + userWalletId: UserWalletId, + onRight: suspend (CryptoCurrencyStatus) -> Unit, +) { + this(userWalletId = userWalletId) + .conflate() + .distinctUntilChanged() + .collectLatest { maybeStatus -> + maybeStatus.onRight { onRight(it) } + } +} \ 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/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt new file mode 100644 index 0000000000..a6d0db40dc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +internal class WalletContentLoaderFactory @Inject constructor( + private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory, + private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory, + private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean = false, + ): WalletContentLoader? { + return when { + userWallet.isMultiCurrency -> { + multiWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents) + } + userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { + singleWalletWithTokenContentLoaderFactory.create(userWallet, appCurrency, clickIntents) + } + !userWallet.isMultiCurrency -> { + singleWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents, isRefresh) + } + else -> null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt new file mode 100644 index 0000000000..11414466c8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.Job +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class WalletLoaderStorage @Inject constructor() { + + private val loaders = ConcurrentHashMap>() + + fun contains(id: UserWalletId) = loaders.containsKey(id) + + fun set(id: UserWalletId, jobs: List) { + loaders[id] = jobs + } + + fun remove(id: UserWalletId) { + loaders[id]?.let { + it.forEach(Job::cancel) + loaders.remove(id) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt new file mode 100644 index 0000000000..24b6e550ed --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -0,0 +1,91 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import timber.log.Timber +import javax.inject.Inject + +/** + * Base wallet screen content loader. Use it to load content by [UserWallet]. + * + * @property factory factory that creates loader + * @property storage storage that save loader's jobs + * @property dispatchers coroutine dispatchers provider + * +[REDACTED_AUTHOR] + */ +@ViewModelScoped +internal class WalletScreenContentLoader @Inject constructor( + private val factory: WalletContentLoaderFactory, + private val storage: WalletLoaderStorage, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Load content by [UserWallet] + * + * @param userWallet user wallet + * @param appCurrency app currency + * @param clickIntents click intents + * @param isRefresh flag that determinate if content must load again + * @param coroutineScope coroutine scope + */ + fun load( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean = false, + coroutineScope: CoroutineScope, + ) { + if (userWallet.isLocked) return + + val id = userWallet.walletId + if (!storage.contains(id)) { + loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, isRefresh) + } else { + if (isRefresh) { + storage.remove(id) + loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, true) + } else { + Timber.d("$id content loading has already started") + } + } + } + + /** Cancel loading by [id] */ + fun cancel(id: UserWalletId) { + Timber.d("$id content loading is canceled") + storage.remove(id) + } + + private fun loadInternal( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + coroutineScope: CoroutineScope, + isRefresh: Boolean, + ) { + val loader = factory.create( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + isRefresh = isRefresh, + ) + + if (loader == null) { + Timber.e("Impossible to create loader for $userWallet") + return + } + + Timber.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") + + loader.subscribers + .map { it.subscribe(coroutineScope, dispatchers) } + .let { storage.set(userWallet.walletId, it) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt new file mode 100644 index 0000000000..6424208a3f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +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.GetMultiWalletWarningsFactory +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.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.TokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class MultiWalletContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getTokenListUseCase: GetTokenListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + TokenListSubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + clickIntents = clickIntents, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getTokenListUseCase = getTokenListUseCase, + ), + MultiWalletWarningsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt new file mode 100644 index 0000000000..1751d80a8b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +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.GetMultiWalletWarningsFactory +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.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +internal class MultiWalletContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + ): WalletContentLoader { + return MultiWalletContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + stateHolder = stateHolder, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getTokenListUseCase = getTokenListUseCase, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt new file mode 100644 index 0000000000..792c45ee35 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -0,0 +1,66 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +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.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class SingleWalletContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val isRefresh: Boolean, + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + PrimaryCurrencySubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, + analyticsEventHandler = analyticsEventHandler, + ), + SingleWalletButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + clickIntents = clickIntents, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, + ), + SingleWalletNotificationsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, + ), + TxHistorySubscriber( + userWallet = userWallet, + isRefresh = isRefresh, + stateHolder = stateHolder, + clickIntents = clickIntents, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, + txHistoryItemsUseCase = txHistoryItemsUseCase, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt new file mode 100644 index 0000000000..05eb03ee25 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +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.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +@Suppress("LongParameterList") +internal class SingleWalletContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean, + ): WalletContentLoader { + return SingleWalletContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + isRefresh = isRefresh, + stateHolder = stateHolder, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, + getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, + setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, + txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, + txHistoryItemsUseCase = txHistoryItemsUseCase, + analyticsEventHandler = analyticsEventHandler, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt new file mode 100644 index 0000000000..fd3242278a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +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.GetMultiWalletWarningsFactory +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.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class SingleWalletWithTokenContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + SingleWalletWithTokenListSubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + clickIntents = clickIntents, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getCardTokensListUseCase = getCardTokensListUseCase, + ), + MultiWalletWarningsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt new file mode 100644 index 0000000000..efef9eeb8d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +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.GetMultiWalletWarningsFactory +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.viewmodels.intents.WalletClickIntentsV2 +import javax.inject.Inject + +internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + ): SingleWalletWithTokenContentLoader { + return SingleWalletWithTokenContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + stateHolder = stateHolder, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getCardTokensListUseCase = getCardTokensListUseCase, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt new file mode 100644 index 0000000000..83f9c28342 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber + +/** + * Wallet content loader + * + * @property id loader id + * +[REDACTED_AUTHOR] + */ +internal abstract class WalletContentLoader(val id: UserWalletId) { + + /** Loader's subscribers */ + val subscribers: List> get() = create() + + protected abstract fun create(): List> +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt index 9c619d9a9c..a269d87651 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -17,4 +17,16 @@ internal sealed class WalletEvent { data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() + + data class DemonstrateWalletsScrollPreview(val direction: Direction) : WalletEvent() { + + enum class Direction { + + /** 1 -> 2 */ + LEFT, + + /** 1 <- 2 */ + RIGHT, + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt new file mode 100644 index 0000000000..740d54798c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt @@ -0,0 +1,187 @@ +package com.tangem.feature.wallet.presentation.wallet.state2 + +import androidx.paging.PagingData +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import javax.annotation.concurrent.Immutable + +const val NOT_INITIALIZED_WALLET_INDEX = -1 + +internal data class WalletScreenState( + val onBackClick: () -> Unit, + val topBarConfig: WalletTopBarConfig, + val selectedWalletIndex: Int, + val wallets: ImmutableList, + val onWalletChange: (Int) -> Unit, + val event: StateEvent, + val isHidingMode: Boolean, +) + +internal sealed class WalletState { + + abstract val pullToRefreshConfig: WalletPullToRefreshConfig + abstract val walletCardState: WalletCardState + abstract val warnings: ImmutableList + abstract val bottomSheetConfig: TangemBottomSheetConfig? + + sealed class MultiCurrency : WalletState() { + + abstract val tokensListState: WalletTokensListState + abstract val manageTokensButtonConfig: ManageTokensButtonConfig? + + data class Content( + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val walletCardState: WalletCardState, + override val warnings: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val tokensListState: WalletTokensListState, + override val manageTokensButtonConfig: ManageTokensButtonConfig?, + ) : MultiCurrency() + + data class Locked( + override val walletCardState: WalletCardState, + val onUnlockNotificationClick: () -> Unit, + val isBottomSheetShow: Boolean = false, + val onBottomSheetDismiss: () -> Unit = {}, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : MultiCurrency() { + + override val pullToRefreshConfig: WalletPullToRefreshConfig + get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + + override val warnings: ImmutableList = persistentListOf( + WalletNotification.UnlockWallets(onUnlockNotificationClick), + ) + + override val bottomSheetConfig = TangemBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val tokensListState = WalletTokensListState.ContentState.Locked + override val manageTokensButtonConfig = null + } + } + + sealed class SingleCurrency : WalletState() { + + abstract val buttons: PersistentList + abstract val marketPriceBlockState: MarketPriceBlockState? + abstract val txHistoryState: TxHistoryState + + data class Content( + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val walletCardState: WalletCardState, + override val warnings: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val buttons: PersistentList, + override val marketPriceBlockState: MarketPriceBlockState, + override val txHistoryState: TxHistoryState, + ) : SingleCurrency() + + data class Locked( + override val walletCardState: WalletCardState, + override val buttons: PersistentList, + val onUnlockNotificationClick: () -> Unit, + val isBottomSheetShow: Boolean = false, + val onBottomSheetDismiss: () -> Unit = {}, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + val onExploreClick: () -> Unit, + ) : SingleCurrency() { + + override val pullToRefreshConfig: WalletPullToRefreshConfig + get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + + override val warnings: ImmutableList = persistentListOf( + WalletNotification.UnlockWallets(onUnlockNotificationClick), + ) + + override val bottomSheetConfig = TangemBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val marketPriceBlockState: MarketPriceBlockState? = null + + override val txHistoryState: TxHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from( + data = listOf( + TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Locked(txHash = "LOCKED_TX_HASH"), + ), + ), + ), + ), + ) + } + } +} + +internal sealed class WalletTokensListState { + + object Empty : WalletTokensListState() + + sealed class ContentState : WalletTokensListState() { + + abstract val items: ImmutableList + abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig? + + object Loading : ContentState() { + override val items = persistentListOf() + override val organizeTokensButtonConfig = null + } + + data class Content( + override val items: ImmutableList, + override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, + ) : ContentState() + + object Locked : ContentState() { + override val items = persistentListOf( + TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")), + ) + override val organizeTokensButtonConfig = null + } + } + + data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit) + + @Immutable + sealed class TokensListItemState { + + abstract val id: Any + + data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState() + + data class Token(val state: TokenItemState) : TokensListItemState() { + override val id: String = state.id + } + } +} + +internal data class ManageTokensButtonConfig(val onClick: () -> Unit) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt new file mode 100644 index 0000000000..c956da9c5d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.wallet.state2 + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.WalletScreenStateTransformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Wallet state holder + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class WalletStateHolderV2 @Inject constructor() { + + val uiState: StateFlow get() = mutableUiState + val value: WalletScreenState get() = uiState.value + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + fun update(function: (WalletScreenState) -> WalletScreenState) { + mutableUiState.update(function = function) + } + + fun update(transformer: WalletScreenStateTransformer) { + mutableUiState.update(function = transformer::transform) + } + + fun getSelectedWallet(): WalletState { + return with(value) { wallets[selectedWalletIndex] } + } + + fun getSelectedWalletId(): UserWalletId { + return with(value) { wallets[selectedWalletIndex].walletCardState.id } + } + + private fun getInitialState(): WalletScreenState { + return WalletScreenState( + onBackClick = {}, + topBarConfig = WalletTopBarConfig(onDetailsClick = {}), + selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, + wallets = persistentListOf(), + onWalletChange = {}, + event = consumedEvent(), + isHidingMode = false, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt new file mode 100644 index 0000000000..437c8d5e41 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList + +internal class AddWalletTransformer( + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { + WalletLoadingStateFactory(clickIntents = clickIntents) + } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt new file mode 100644 index 0000000000..37ddf91e8c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false)) + } + is WalletState.MultiCurrency.Locked -> prevState.copy(isBottomSheetShow = false) + is WalletState.SingleCurrency.Content -> { + prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false)) + } + is WalletState.SingleCurrency.Locked -> prevState.copy(isBottomSheetShow = false) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt new file mode 100644 index 0000000000..34b8081a57 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class DeleteWalletTransformer( + private val selectedWalletIndex: Int, + private val deletedWalletId: UserWalletId, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val deletedWalletState = prevState.getDeletedWalletState() + + if (deletedWalletState == null) { + Timber.e("Wallets does not contain deleted wallet") + return prevState + } + + return prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + ) + } + + private fun WalletScreenState.getDeletedWalletState(): WalletState? { + return wallets.firstOrNull { it.walletCardState.id == deletedWalletId } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt new file mode 100644 index 0000000000..439a633944 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt @@ -0,0 +1,97 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.state2.utils.createStateByWalletType +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class InitializeWalletsTransformer( + private val selectedWalletIndex: Int, + private val selectedWallet: UserWallet, + private val wallets: List, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + onBackClick = clickIntents::onBackClick, + topBarConfig = createTopBarConfig(userWallet = selectedWallet), + selectedWalletIndex = selectedWalletIndex, + wallets = wallets + .map { userWallet -> + if (userWallet.isLocked) { + createLockedState(userWallet) + } else { + walletLoadingStateFactory.create(userWallet) + } + } + .toImmutableList(), + onWalletChange = clickIntents::onWalletChange, + ) + } + + private fun createTopBarConfig(userWallet: UserWallet): WalletTopBarConfig { + return WalletTopBarConfig( + onDetailsClick = if (userWallet.isLocked) { + clickIntents::onOpenUnlockWalletsBottomSheetClick + } else { + clickIntents::onDetailsClick + }, + ) + } + + private fun createLockedState(userWallet: UserWallet): WalletState { + return userWallet.createStateByWalletType( + multiCurrencyCreator = { + WalletState.MultiCurrency.Locked( + walletCardState = userWallet.toLockedWalletCardState(), + onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, + ) + }, + singleCurrencyCreator = { + WalletState.SingleCurrency.Locked( + walletCardState = userWallet.toLockedWalletCardState(), + buttons = createDisabledButtons(), + onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, + onExploreClick = clickIntents::onExploreClick, + ) + }, + ) + } + + private fun UserWallet.toLockedWalletCardState(): WalletCardState { + return WalletCardState.LockedContent( + id = walletId, + title = name, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = this), + imageResId = WalletImageResolver.resolve(userWallet = this), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, + ) + } + + private fun createDisabledButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt new file mode 100644 index 0000000000..f48797a50f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal class OpenBottomSheetTransformer( + userWalletId: UserWalletId, + private val content: TangemBottomSheetConfigContent, + private val onDismissBottomSheet: () -> Unit, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismissBottomSheet, + content = content, + ), + ) + } + is WalletState.MultiCurrency.Locked -> { + prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismissBottomSheet, + content = content, + ), + ) + } + is WalletState.SingleCurrency.Locked -> { + prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt new file mode 100644 index 0000000000..ad0ef39b08 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.persistentListOf + +/** +[REDACTED_AUTHOR] + */ +internal class ReinitializeWalletTransformer( + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = persistentListOf( + walletLoadingStateFactory.create(userWallet), + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt new file mode 100644 index 0000000000..80b3692e93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import timber.log.Timber + +internal class RenameWalletTransformer( + userWalletId: UserWalletId, + private val newName: String, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName)) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName)) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to rename wallet in locked state") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt new file mode 100644 index 0000000000..17c76d98e5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.common.Provider +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class ScrollToWalletTransformer( + private val index: Int, + private val currentStateProvider: Provider, + private val stateUpdater: (WalletScreenState) -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + event = triggeredEvent( + data = WalletEvent.ChangeWallet(index), + onConsume = { + stateUpdater( + currentStateProvider().copy( + selectedWalletIndex = index, + event = consumedEvent(), + ), + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt new file mode 100644 index 0000000000..f34236635b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class SendEventTransformer( + private val event: WalletEvent, + private val onConsume: () -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + event = triggeredEvent(data = event, onConsume = onConsume), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt new file mode 100644 index 0000000000..605e40f98a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -0,0 +1,81 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class SetCryptoCurrencyActionsTransformer( + private val tokenActionsState: TokenActionsState, + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy(buttons = tokenActionsState.toManageButtons()) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load crypto currency actions for multi-currency wallet") + prevState + } + } + } + + private fun TokenActionsState.toManageButtons(): PersistentList { + return states + .filterIfS2C() + .mapNotNull { action -> + when (action) { + is TokenActionsState.ActionState.Buy -> { + WalletManageButton.Buy( + enabled = action.enabled, + onClick = { clickIntents.onBuyClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Receive -> { + WalletManageButton.Receive( + enabled = action.enabled, + onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Sell -> { + WalletManageButton.Sell( + enabled = action.enabled, + onClick = { clickIntents.onSellClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Send -> { + WalletManageButton.Send( + enabled = action.enabled, + onClick = { clickIntents.onSendClick(cryptoCurrencyStatus) }, + ) + } + else -> { + null + } + } + } + .toPersistentList() + } + + private fun List.filterIfS2C(): List { + return if (userWallet.scanResponse.cardTypesResolver.isStart2Coin()) { + filterNot { it is TokenActionsState.ActionState.Buy || it is TokenActionsState.ActionState.Sell } + } else { + this + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt new file mode 100644 index 0000000000..83f89adc93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletMarketPriceConverter +import timber.log.Timber + +internal class SetPrimaryCurrencyTransformer( + private val userWallet: UserWallet, + private val status: CryptoCurrencyStatus.Status, + private val appCurrency: AppCurrency, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + marketPriceBlockState = prevState.marketPriceBlockState.toLoadedState(), + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load primary currency status for multi-currency wallet") + prevState + } + } + } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return SingleWalletCardStateConverter(status, userWallet, appCurrency).convert(value = this) + } + + private fun MarketPriceBlockState.toLoadedState(): MarketPriceBlockState { + return SingleWalletMarketPriceConverter(status, appCurrency).convert(value = this) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt new file mode 100644 index 0000000000..043c611ccd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate + +internal class SetRefreshStateTransformer( + userWalletId: UserWalletId, + private val isRefreshing: Boolean, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), + tokensListState = prevState.tokensListState.toUpdatedState(), + ) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), + buttons = prevState.buttons.toUpdatedState(), + ) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> prevState + } + } + + private fun WalletPullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): WalletPullToRefreshConfig { + return copy(isRefreshing = isRefreshing) + } + + private fun WalletTokensListState.toUpdatedState(): WalletTokensListState { + return if (this is WalletTokensListState.ContentState.Content && organizeTokensButtonConfig != null) { + copy( + organizeTokensButtonConfig = organizeTokensButtonConfig.copy( + isEnabled = !isRefreshing, + ), + ) + } else { + this + } + } + + private fun PersistentList.toUpdatedState(): PersistentList { + val isButtonsEnabled = !isRefreshing + + return mutate { + it.mapNotNull { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Receive -> button + is WalletManageButton.Swap -> null + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt new file mode 100644 index 0000000000..4586c30ab6 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import timber.log.Timber + +internal class SetTokenListErrorTransformer( + userWalletId: UserWalletId, + private val error: TokenListError, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (error) { + is TokenListError.EmptyTokens -> { + when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(tokensListState = WalletTokensListState.Empty) + } + is WalletState.MultiCurrency.Locked, + -> { + Timber.e("Impossible to load tokens list for locked wallet") + prevState + } + is WalletState.SingleCurrency, + -> { + Timber.e("Impossible to load tokens list for single-currency wallet") + prevState + } + } + } + is TokenListError.DataError, + is TokenListError.UnableToSortTokenList, + -> prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt new file mode 100644 index 0000000000..f982dff23f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt @@ -0,0 +1,69 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import timber.log.Timber + +internal class SetTokenListTransformer( + private val tokenList: TokenList, + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + tokensListState = prevState.tokensListState.toLoadedState(), + manageTokensButtonConfig = createManageTokensButtonConfig(), + ) + } + is WalletState.MultiCurrency.Locked, + -> { + Timber.e("Impossible to load tokens list for locked wallet") + prevState + } + is WalletState.SingleCurrency, + -> { + Timber.e("Impossible to load tokens list for single-currency wallet") + prevState + } + } + } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return MultiWalletCardStateConverter( + fiatBalance = tokenList.totalFiatBalance, + selectedWallet = userWallet, + appCurrency = appCurrency, + ).convert(value = this) + } + + private fun WalletTokensListState.toLoadedState(): WalletTokensListState { + return TokenListStateConverter( + tokenList = tokenList, + selectedWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ).convert(value = this) + } + + private fun createManageTokensButtonConfig(): ManageTokensButtonConfig? { + return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + null + } else { + ManageTokensButtonConfig(clickIntents::onManageTokensClick) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt new file mode 100644 index 0000000000..26f55b70a4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class SetTxHistoryCountErrorTransformer( + private val userWallet: UserWallet, + private val error: TxHistoryStateError, + private val pendingTransactions: Set, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + private val txHistoryItemConverter by lazy { + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + TxHistoryItemStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + clickIntents = clickIntents, + ) + } + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> prevState.toErrorState() + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } + + private fun WalletState.SingleCurrency.Content.toErrorState(): WalletState { + return copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + TxHistoryState.Empty(onExploreClick = clickIntents::onExploreClick) + } + is TxHistoryStateError.DataError -> { + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + TxHistoryState.NotSupported( + pendingTransactions = txHistoryItemConverter.convertList(pendingTransactions) + .toImmutableList(), + onExploreClick = clickIntents::onExploreClick, + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt new file mode 100644 index 0000000000..082d5aa124 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import androidx.paging.PagingData +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber + +internal class SetTxHistoryCountTransformer( + userWalletId: UserWalletId, + private val transactionsCount: Int, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> prevState.toLoadingState() + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } + + private fun WalletState.SingleCurrency.Content.toLoadingState(): WalletState { + return if (txHistoryState is TxHistoryState.Content) { + (txHistoryState as? TxHistoryState.Content)?.contentItems?.update { + Timber.d("Load transactions history: $transactionsCount") + PagingData.from(data = createLoadingItems()) + } + this + } else { + val txHistoryContent = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from(data = createLoadingItems()), + ), + ) + copy(txHistoryState = txHistoryContent) + } + } + + private fun createLoadingItems(): List { + return buildList { + add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + (1..transactionsCount).forEach { + add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt new file mode 100644 index 0000000000..37e26fa534 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import timber.log.Timber + +internal class SetTxHistoryItemsErrorTransformer( + userWalletId: UserWalletId, + private val error: TxHistoryListError, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + }, + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt new file mode 100644 index 0000000000..1bd03fe9b1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import androidx.paging.PagingData +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemFlowConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.flow.Flow +import timber.log.Timber + +internal class SetTxHistoryItemsTransformer( + private val userWallet: UserWallet, + private val flow: Flow>, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + val converter = TxHistoryItemFlowConverter( + userWallet = userWallet, + currentState = prevState, + clickIntents = clickIntents, + ) + prevState.copy( + txHistoryState = converter.convert(value = flow), + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt new file mode 100644 index 0000000000..064c72d1e7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.ImmutableList +import timber.log.Timber + +internal class SetWarningsTransformer( + userWalletId: UserWalletId, + private val warnings: ImmutableList, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy(warnings = warnings) + is WalletState.SingleCurrency.Content -> prevState.copy(warnings = warnings) + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to update notifications for locked wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt new file mode 100644 index 0000000000..bd5c0bd61b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class UnlockWalletTransformer( + private val unlockedWallets: List, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + topBarConfig = prevState.topBarConfig.toUnlockedState(), + wallets = prevState.wallets + .map { state -> + val unlockedWallet = getUnlockedWallet(state.walletCardState.id) + if (unlockedWallet == null) state else createLoadingState(state, unlockedWallet) + } + .toImmutableList(), + ) + } + + private fun WalletTopBarConfig.toUnlockedState(): WalletTopBarConfig { + return copy(onDetailsClick = clickIntents::onDetailsClick) + } + + private fun getUnlockedWallet(walletId: UserWalletId): UserWallet? { + return unlockedWallets.firstOrNull { it.walletId == walletId } + } + + private fun createLoadingState(prevState: WalletState, unlockedWallet: UserWallet): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> walletLoadingStateFactory.create(userWallet = unlockedWallet) + is WalletState.MultiCurrency.Content, + is WalletState.SingleCurrency.Content, + -> { + Timber.e("Impossible to unlock wallet with content state") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt new file mode 100644 index 0000000000..6dd7da1e64 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class UpdateBalanceHidingModeTransformer( + private val isHidingMode: Boolean, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy(isHidingMode = isHidingMode) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt new file mode 100644 index 0000000000..fe89479e52 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import timber.log.Timber + +internal class UpdateWalletCardsCountTransformer( + private val userWallet: UserWallet, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to update wallet cards count for locked wallet") + prevState + } + } + } + + private fun WalletCardState.toUpdatedState(): WalletCardState { + return when (this) { + is WalletCardState.Content -> copy( + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + imageResId = WalletImageResolver.resolve(userWallet = userWallet), + cardCount = userWallet.getCardsCount(), + ) + else -> this + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt new file mode 100644 index 0000000000..05cbc33f27 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal interface WalletScreenStateTransformer { + + fun transform(prevState: WalletScreenState): WalletScreenState +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt new file mode 100644 index 0000000000..38af9d7f93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.toImmutableList + +internal abstract class WalletStateTransformer( + protected val userWalletId: UserWalletId, +) : WalletScreenStateTransformer { + + abstract fun transform(prevState: WalletState): WalletState + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = prevState.wallets + .map { state -> + if (state.walletCardState.id == userWalletId) transform(state) else state + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt new file mode 100644 index 0000000000..8c3b84bf05 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.utils.converter.Converter + +internal class MultiWalletCardStateConverter( + private val fiatBalance: TokenList.FiatBalance, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: WalletCardState): WalletCardState { + return when (fiatBalance) { + is TokenList.FiatBalance.Loading -> value.toLoadingState() + is TokenList.FiatBalance.Failed -> value.toErrorState() + is TokenList.FiatBalance.Loaded -> value.toWalletCardState(fiatBalance) + } + } + + private fun WalletCardState.toLoadingState(): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toErrorState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onDeleteClick = onDeleteClick, + onRenameClick = onRenameClick, + ) + } + + private fun WalletCardState.toWalletCardState(fiatBalance: TokenList.FiatBalance.Loaded): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatBalance.amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + cardCount = selectedWallet.getCardsCount(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt new file mode 100644 index 0000000000..270fa05c9c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -0,0 +1,96 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletCurrencyActionsClickIntentsImplementor +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class MultiWalletCurrencyActionsConverter( + private val userWallet: UserWallet, + private val clickIntents: WalletCurrencyActionsClickIntentsImplementor, +) : Converter> { + + override fun convert(value: TokenActionsState): ImmutableList { + return value.states + .filterIfSingleWithToken() + .mapNotNull { + mapTokenActionState(actionsState = it, cryptoCurrencyStatus = value.cryptoCurrencyStatus) + } + .toImmutableList() + } + + private fun List.filterIfSingleWithToken(): List { + return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + filter { it !is TokenActionsState.ActionState.HideToken } + } else { + this + } + } + + private fun mapTokenActionState( + actionsState: TokenActionsState.ActionState, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): TokenActionButtonConfig? { + if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) { + return null + } + + val title: TextReference + val icon: Int + val action: () -> Unit + when (actionsState) { + is TokenActionsState.ActionState.Buy -> { + title = resourceReference(R.string.common_buy) + icon = R.drawable.ic_plus_24 + action = { clickIntents.onBuyClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Receive -> { + title = resourceReference(R.string.common_receive) + icon = R.drawable.ic_arrow_down_24 + action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Sell -> { + title = resourceReference(R.string.common_sell) + icon = R.drawable.ic_currency_24 + action = { clickIntents.onSellClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Send -> { + title = resourceReference(R.string.common_send) + icon = R.drawable.ic_arrow_up_24 + action = { clickIntents.onSendClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Swap -> { + title = resourceReference(R.string.common_swap) + icon = R.drawable.ic_exchange_horizontal_24 + action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.CopyAddress -> { + title = resourceReference(R.string.common_copy_address) + icon = R.drawable.ic_copy_24 + action = { clickIntents.onCopyAddressClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.HideToken -> { + title = resourceReference(R.string.token_details_hide_token) + icon = R.drawable.ic_hide_24 + action = { clickIntents.onHideTokensClick(cryptoCurrencyStatus) } + } + } + + return TokenActionButtonConfig( + text = title, + iconResId = icon, + onClick = action, + isWarning = actionsState is TokenActionsState.ActionState.HideToken, + enabled = actionsState.enabled, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt new file mode 100644 index 0000000000..e54cf09dfa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt @@ -0,0 +1,78 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.utils.converter.Converter + +internal class SingleWalletCardStateConverter( + private val status: CryptoCurrencyStatus.Status, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: WalletCardState): WalletCardState { + return when (status) { + is CryptoCurrencyStatus.Loading -> value.toLoadingState() + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + -> value.toErrorState() + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, + -> value.toContentState(status) + } + } + + private fun WalletCardState.toLoadingState(): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toErrorState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toContentState(status: CryptoCurrencyStatus.Status): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = selectedWallet, + currencyAmount = status.amount, + ), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrency), + cardCount = selectedWallet.getCardsCount(), + ) + } + + private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt new file mode 100644 index 0000000000..1f1237f976 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SingleWalletMarketPriceConverter( + private val status: CryptoCurrencyStatus.Status, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: MarketPriceBlockState): MarketPriceBlockState { + return when (status) { + CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(value.currencySymbol) + is CryptoCurrencyStatus.NoAccount -> value.toNoAccountState() + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAmount, + -> value.toContentState() + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Unreachable, + -> MarketPriceBlockState.Error(value.currencySymbol) + } + } + + private fun MarketPriceBlockState.toNoAccountState(): MarketPriceBlockState { + return if (status.fiatRate == null) MarketPriceBlockState.Error(currencySymbol) else toContentState() + } + + private fun MarketPriceBlockState.toContentState(): MarketPriceBlockState { + return MarketPriceBlockState.Content( + currencySymbol = currencySymbol, + price = formatPrice(status = status, appCurrency = appCurrency), + priceChangeConfig = PriceChangeState.Content( + valueInPercent = formatPriceChange(status = status), + type = getPriceChangeType(status = status), + ), + ) + } + + private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRate, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) + } + + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + val priceChange = status.priceChange ?: return PriceChangeType.DOWN + + return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt new file mode 100644 index 0000000000..eef6aea5c9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt @@ -0,0 +1,122 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenItemStateConverter( + private val appCurrencyProvider: Provider, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + override fun convert(value: CryptoCurrencyStatus): TokenItemState { + return when (value.value) { + is CryptoCurrencyStatus.Loading -> value.mapToLoadingState() + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> value.mapToTokenItemState() + is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState() + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> value.mapToUnreachableTokenItemState() + } + } + + private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading { + return TokenItemState.Loading( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + ) + } + + private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { + return TokenItemState.Content( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content( + text = currency.name, + hasPending = value.hasCurrentNetworkTransactions, + ), + fiatAmountState = TokenItemState.FiatAmountState.Content( + text = getFormattedFiatAmount(), + ), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), + cryptoPriceState = getCryptoPriceState(), + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + } + + private fun CryptoCurrencyStatus.getFormattedAmount(): String { + val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + + private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val appCurrency = appCurrencyProvider() + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + } + + private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + + private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState() = TokenItemState.NoAddress( + id = currency.id.value, + iconState = iconStateConverter.convert(this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + val fiatRate = value.fiatRate + val priceChange = value.priceChange + + return if (fiatRate != null && priceChange != null) { + TokenItemState.CryptoPriceState.Content( + price = fiatRate.getFormattedCryptoPrice(), + priceChangePercent = BigDecimalFormatter.formatPercent( + percent = priceChange, + useAbsoluteValue = true, + maxFractionDigits = 1, + minFractionDigits = 1, + ), + type = priceChange.getPriceChangeType(), + ) + } else { + TokenItemState.CryptoPriceState.Unknown + } + } + + private fun BigDecimal.getFormattedCryptoPrice(): String { + val appCurrency = appCurrencyProvider() + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = this, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun BigDecimal.getPriceChangeType(): PriceChangeType { + return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt new file mode 100644 index 0000000000..b9b2ab5cf3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt @@ -0,0 +1,93 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +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.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig + +internal class TokenListStateConverter( + private val tokenList: TokenList, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + private val tokenStatusConverter = TokenItemStateConverter( + appCurrencyProvider = Provider { appCurrency }, + clickIntents = clickIntents, + ) + + override fun convert(value: WalletTokensListState): WalletTokensListState { + return when (tokenList) { + is TokenList.Empty -> WalletTokensListState.Empty + is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content( + items = tokenList.toGroupedItems(), + organizeTokensButtonConfig = getOrganizeTokensButtonState( + currenciesSize = tokenList.groups.flatMap(NetworkGroup::currencies).size, + ), + ) + is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content( + items = tokenList.toUngroupedItems(), + organizeTokensButtonConfig = getOrganizeTokensButtonState(currenciesSize = tokenList.currencies.size), + ) + } + } + + private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { + return groups.fold(initial = persistentListOf()) { acc, group -> + acc.mutate { it.addGroup(group) } + } + } + + private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { + return currencies.fold(initial = persistentListOf()) { acc, token -> + acc.mutate { it.addToken(token) } + } + } + + private fun MutableList.addGroup(group: NetworkGroup): List { + val groupTitle = TokensListItemState.NetworkGroupTitle( + id = group.network.hashCode(), + name = stringReference(group.network.name), + ) + + add(groupTitle) + group.currencies.forEach { token -> addToken(token) } + + return this + } + + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { + val tokenItemState = tokenStatusConverter.convert(token) + add(TokensListItemState.Token(tokenItemState)) + + return this + } + + private fun getOrganizeTokensButtonState(currenciesSize: Int): WalletOrganizeTokensButtonConfig? { + return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) { + WalletOrganizeTokensButtonConfig( + isEnabled = tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } + + private fun isSingleCurrencyWalletWithToken(): Boolean { + return selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..6aeb577d67 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt @@ -0,0 +1,118 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import androidx.paging.* +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import java.util.UUID + +private val scope = CoroutineScope(Dispatchers.IO) + +internal class TxHistoryItemFlowConverter( + private val userWallet: UserWallet, + private val currentState: WalletState.SingleCurrency.Content, + private val clickIntents: WalletClickIntentsV2, +) : Converter>, TxHistoryState?> { + + private val txHistoryItemConverter by lazy { + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + TxHistoryItemStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + clickIntents = clickIntents, + ) + } + + override fun convert(value: Flow>): TxHistoryState { + val txHistoryContent = currentState.txHistoryState as? TxHistoryState.Content + ?: TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + } + } + .cachedIn(scope) + .launchIn(scope) + + return txHistoryContent + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return txHistoryItemConverter.convert(value = item) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString()) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString()) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + val txContent = txHistoryItemState.state as TransactionState.Content + txHistoryItemState.copy( + state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()), + ) + } else { + txHistoryItemState + } + } + } + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt new file mode 100644 index 0000000000..e9c78803f3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt @@ -0,0 +1,109 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString + +internal class TxHistoryItemStateConverter( + private val symbol: String, + private val decimals: Int, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + override fun convert(value: TxHistoryItem): TransactionState { + return createTransactionStateItem(item = value) + } + + @Suppress("LongMethod") + private fun createTransactionStateItem(item: TxHistoryItem): TransactionState { + return TransactionState.Content( + txHash = item.txHash, + amount = item.getAmount(), + timestamp = item.getRawTimestamp(), + status = item.status.tiUiStatus(), + direction = item.extractDirection(), + iconRes = item.extractIcon(), + title = item.extractTitle(), + subtitle = item.extractSubtitle(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, + ) + } + + private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) { + R.drawable.ic_close_24 + } else { + when (type) { + is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24 + is TxHistoryItem.TransactionType.Operation, + is TxHistoryItem.TransactionType.Swap, + is TxHistoryItem.TransactionType.Transfer, + is TxHistoryItem.TransactionType.UnknownOperation, + -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 + } + } + + private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) { + is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval) + is TxHistoryItem.TransactionType.Operation -> stringReference(type.name) + is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap) + is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer) + is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) + } + + private fun TxHistoryItem.extractSubtitle(): TextReference = + when (val interactionAddress = interactionAddressType) { + is TxHistoryItem.InteractionAddressType.Contract -> resourceReference( + id = R.string.transaction_history_contract_address, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), + ) + is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference( + id = if (isOutgoing) { + R.string.transaction_history_transaction_to_address + } else { + R.string.transaction_history_transaction_from_address + }, + formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)), + ) + is TxHistoryItem.InteractionAddressType.User -> resourceReference( + id = if (isOutgoing) { + R.string.transaction_history_transaction_to_address + } else { + R.string.transaction_history_transaction_from_address + }, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), + ) + } + + private fun TxHistoryItem.extractDirection() = + if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() + + private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) { + TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed + TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed + TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed + } + + private fun TxHistoryItem.getAmount(): String { + val prefix = when (status) { + TxHistoryItem.TransactionStatus.Failed -> "" + else -> if (isOutgoing) "-" else "+" + } + return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt new file mode 100644 index 0000000000..75d10c7b80 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal inline fun UserWallet.createStateByWalletType( + multiCurrencyCreator: () -> WalletState.MultiCurrency, + singleCurrencyCreator: () -> WalletState.SingleCurrency, +): WalletState { + return if (isWalletWithTokens()) multiCurrencyCreator() else singleCurrencyCreator() +} + +private fun UserWallet.isWalletWithTokens(): Boolean { + return isMultiCurrency || scanResponse.cardTypesResolver.isSingleWalletWithToken() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt new file mode 100644 index 0000000000..6cc3953821 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SendEventTransformer +import javax.inject.Inject + +/** + * Component for sending events [WalletEvent] on WalletScreen + * + * @property stateHolder state holder for changing state + * +[REDACTED_AUTHOR] + */ +internal class WalletEventSender @Inject constructor( + private val stateHolder: WalletStateHolderV2, +) { + + fun send(event: WalletEvent) { + stateHolder.update(transformer = SendEventTransformer(event = event, onConsume = ::onConsume)) + } + + private fun onConsume() { + stateHolder.update { + it.copy(event = consumedEvent()) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt new file mode 100644 index 0000000000..055d23befa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt @@ -0,0 +1,85 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Factory for creating loading state [WalletState] + * + * @property clickIntents click intents + */ +internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIntentsV2) { + + fun create(userWallet: UserWallet): WalletState { + return userWallet.createStateByWalletType( + multiCurrencyCreator = { createLoadingMultiCurrencyContent(userWallet) }, + singleCurrencyCreator = { createLoadingSingleCurrencyContent(userWallet) }, + ) + } + + private fun createLoadingMultiCurrencyContent(userWallet: UserWallet): WalletState.MultiCurrency.Content { + return WalletState.MultiCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = userWallet.toLoadingWalletCardState(), + warnings = persistentListOf(), + bottomSheetConfig = null, + tokensListState = WalletTokensListState.ContentState.Loading, + manageTokensButtonConfig = ManageTokensButtonConfig(clickIntents::onManageTokensClick), + ) + } + + private fun createLoadingSingleCurrencyContent(userWallet: UserWallet): WalletState.SingleCurrency.Content { + val currencySymbol = userWallet.scanResponse.cardTypesResolver.getBlockchain().currency + return WalletState.SingleCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = userWallet.toLoadingWalletCardState(), + warnings = persistentListOf(), + bottomSheetConfig = null, + buttons = createDisabledButtons(), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = currencySymbol), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { + return WalletPullToRefreshConfig(onRefresh = clickIntents::onRefreshSwipe, isRefreshing = false) + } + + private fun UserWallet.toLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading( + id = walletId, + title = name, + additionalInfo = if (isMultiCurrency) WalletAdditionalInfoFactory.resolve(wallet = this) else null, + imageResId = WalletImageResolver.resolve(userWallet = this), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, + ) + } + + private fun createDisabledButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index a6a26d1828..dbb5f28cfa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -66,6 +66,7 @@ internal fun WalletEventEffect( } .addOnFailureListener(Timber::e) } + is WalletEvent.DemonstrateWalletsScrollPreview -> Unit } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt new file mode 100644 index 0000000000..73d54f4ddc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import android.widget.Toast +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester +import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex +import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling + +@Suppress("LongParameterList") +@Composable +internal fun WalletEventEffectV2( + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + event: StateEvent, + selectedWalletIndex: Int, + onAutoScrollSet: () -> Unit, + onAlertConfigSet: (WalletAlertState) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = LocalContext.current.resources + val clipboardManager = LocalClipboardManager.current + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is WalletEvent.ChangeWallet -> { + onAutoScrollSet() + walletsListState.animateScrollByIndex(prevIndex = selectedWalletIndex, newIndex = value.index) + } + is WalletEvent.ShowError -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is WalletEvent.ShowToast -> { + Toast.makeText(context, value.text.resolveReference(resources), Toast.LENGTH_SHORT).show() + } + is WalletEvent.CopyAddress -> { + clipboardManager.setText(AnnotatedString(value.address)) + Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() + } + is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) + is WalletEvent.RateApp -> { + ReviewManagerRequester.request(context = context, onDismissClick = value.onDismissClick) + } + is WalletEvent.DemonstrateWalletsScrollPreview -> { + walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction) + } + } + }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt new file mode 100644 index 0000000000..fc946a27f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt @@ -0,0 +1,243 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock +import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun WalletScreenV2(state: WalletScreenState) { + BackHandler(onBack = state.onBackClick) + + // It means that screen is still initializing + if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return + + val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex) + val snackbarHostState = remember(::SnackbarHostState) + val isAutoScroll = remember { mutableStateOf(value = false) } + + WalletContent( + state = state, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + isAutoScroll = isAutoScroll, + onAutoScrollReset = { isAutoScroll.value = false }, + ) + + var alertConfig by remember { mutableStateOf(value = null) } + + alertConfig?.let { + WalletAlert(state = it, onDismiss = { alertConfig = null }) + } + + WalletEventEffectV2( + event = state.event, + selectedWalletIndex = state.selectedWalletIndex, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + onAlertConfigSet = { alertConfig = it }, + onAutoScrollSet = { isAutoScroll.value = true }, + ) +} + +@Suppress("LongMethod") +@Composable +private fun WalletContent( + state: WalletScreenState, + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) } + val selectedWallet = state.wallets[selectedWalletIndex] + + BaseScaffold(state = state, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState) { + val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) + + val lazyTxHistoryItems = (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> + (walletState.txHistoryState as? TxHistoryState.Content)?.contentItems?.collectAsLazyPagingItems() + } + + val txHistoryItems by remember(selectedWallet.walletCardState.id, lazyTxHistoryItems?.itemCount) { + mutableStateOf(value = lazyTxHistoryItems) + } + + val betweenItemsPadding = TangemTheme.dimens.spacing14 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = movableItemModifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing92, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item( + key = state.wallets.map { it.walletCardState.id }, + contentType = state.wallets.map { it.walletCardState.id }, + ) { + WalletsList( + lazyListState = walletsListState, + wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), + isBalanceHidden = state.isHidingMode, + ) + } + + (selectedWallet as? WalletState.SingleCurrency)?.let { + controlButtons( + configs = it.buttons, + selectedWalletIndex = selectedWalletIndex, + modifier = movableItemModifier.padding(top = betweenItemsPadding), + ) + } + + notifications(configs = selectedWallet.warnings, modifier = itemModifier) + + (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> + walletState.marketPriceBlockState?.let { marketPriceBlockState -> + marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) + } + } + + contentItemsV2( + state = selectedWallet, + txHistoryItems = txHistoryItems, + isBalanceHidden = state.isHidingMode, + modifier = movableItemModifier, + ) + + organizeTokens(state = selectedWallet, itemModifier = itemModifier) + } + + val bottomSheetConfig = selectedWallet.bottomSheetConfig + if (bottomSheetConfig != null) { + when (bottomSheetConfig.content) { + is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) + is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) + is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) + is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) + } + } + + WalletsListEffectsV2( + lazyListState = walletsListState, + selectedWalletIndex = selectedWalletIndex, + onWalletChange = state.onWalletChange, + onSelectedWalletIndexSet = { selectedWalletIndex = it }, + isAutoScroll = isAutoScroll, + onAutoScrollReset = onAutoScrollReset, + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun BaseScaffold( + state: WalletScreenState, + selectedWallet: WalletState, + snackbarHostState: SnackbarHostState, + content: @Composable () -> Unit, +) { + Scaffold( + topBar = { WalletTopBar(config = state.topBarConfig) }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + floatingActionButton = { + val manageTokensButtonConfig by remember(state.selectedWalletIndex) { + mutableStateOf( + (state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)?.manageTokensButtonConfig, + ) + } + + manageTokensButtonConfig?.let { ManageTokensButton(onClick = it.onClick) } + }, + floatingActionButtonPosition = FabPosition.Center, + containerColor = TangemTheme.colors.background.secondary, + content = { + val pullRefreshState = rememberPullRefreshState( + refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + ) + + Box( + modifier = Modifier + .pullRefresh(pullRefreshState) + .padding(it), + ) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + }, + ) +} + +@Composable +private fun ManageTokensButton(onClick: () -> Unit) { + PrimaryButton( + text = stringResource(id = R.string.main_manage_tokens), + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + ) +} + +internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modifier) { + (state as? WalletState.MultiCurrency)?.let { + (state.tokensListState as? WalletTokensListState.ContentState)?.let { + it.organizeTokensButtonConfig?.let { config -> + organizeTokensButton( + modifier = itemModifier, + isEnabled = config.isEnabled, + onClick = config.onClick, + ) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt new file mode 100644 index 0000000000..d7dc0bb129 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.snapshotFlow +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollectorV2 +import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector + +@Suppress("LongParameterList") +@Composable +internal fun WalletsListEffectsV2( + lazyListState: LazyListState, + selectedWalletIndex: Int, + onWalletChange: (Int) -> Unit, + onSelectedWalletIndexSet: (Int) -> Unit, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) { + snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } + .collect( + collector = ScrollOffsetCollectorV2( + selectedWalletIndex = selectedWalletIndex, + lazyListState = lazyListState, + onWalletChange = { newIndex -> + // Auto scroll must not change wallet + if (isAutoScroll.value) { + onSelectedWalletIndexSet(newIndex) + } else { + onSelectedWalletIndexSet(newIndex) + onWalletChange(newIndex) + } + }, + ), + ) + } + + LaunchedEffect(Unit) { + lazyListState.interactionSource.interactions.collect( + collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 7caf718821..ff70bf8486 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard +import kotlinx.collections.immutable.ImmutableList private const val SHORT_SNAP_ELEMENT_COUNT = 50 @@ -66,6 +68,40 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun WalletsList( + lazyListState: LazyListState, + wallets: ImmutableList, + isBalanceHidden: Boolean, +) { + val horizontalCardPadding = TangemTheme.dimens.spacing16 + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } + + LazyRow( + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + state = lazyListState, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth), + ) { + items( + items = wallets, + key = { it.id.stringValue }, + contentType = { it.id.stringValue }, + ) { state -> + WalletCard( + state = state, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItemPlacement() + .width(itemWidth), + ) + } + } +} + /** * Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'. * Every user's drag action will similar to a short snap diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 13ff628917..8ad776a987 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -9,6 +9,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItemsV2 +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState as WalletStateV2 /** * Wallet content @@ -29,4 +31,20 @@ internal fun LazyListScope.contentItems( is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier, isBalanceHidden) is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) } +} + +internal fun LazyListScope.contentItemsV2( + state: WalletStateV2, + txHistoryItems: LazyPagingItems?, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletStateV2.MultiCurrency -> { + tokensListItemsV2(state.tokensListState, modifier, isBalanceHidden) + } + is WalletStateV2.SingleCurrency -> { + txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index e60ec4b043..9e0f9f5123 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -22,7 +22,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + contentItemsV2( + items = state.items, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + WalletTokensListStateV2.Empty -> nonContentItem(modifier = modifier) + } +} + private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -67,6 +86,28 @@ private fun LazyListScope.contentItems( ) } +private fun LazyListScope.contentItemsV2( + items: ImmutableList, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + isBalanceHidden = isBalanceHidden, + modifier = modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), + ) + }, + ) +} + @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 9678a22d13..48f04fb8ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState /** * Multi-currency content item @@ -29,4 +30,20 @@ internal fun MultiCurrencyContentItem( TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) } } +} + +@Composable +internal fun MultiCurrencyContentItem( + state: TokensListItemState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is TokensListItemState.NetworkGroupTitle -> { + NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) + } + is TokensListItemState.Token -> { + TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt new file mode 100644 index 0000000000..a9ccd192d4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import androidx.compose.foundation.lazy.LazyListState + +/** + * Animate scroll [LazyListState]. + * + * [LazyListState] method for scroll with animation by index isn't supported custom animation. + * This extension method calculate offset between [prevIndex] and [newIndex], + * and scroll by it with default animation. + */ +internal suspend fun LazyListState.animateScrollByIndex(prevIndex: Int, newIndex: Int) { + animateScrollBy( + value = calculateOffset(layoutInfo, prevIndex, newIndex), + animationSpec = tween(durationMillis = 1000), + ) +} + +private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newIndex: Int): Float { + return layoutInfo.viewportSize.width.times(other = newIndex - prevIndex).toFloat() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt new file mode 100644 index 0000000000..a52e61dd42 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.tasks.Task +import timber.log.Timber + +internal object ReviewManagerRequester { + + fun request(context: Context, onDismissClick: () -> Unit) { + val reviewManager = ReviewManagerFactory.create(context) + val requestTask = reviewManager.requestReviewFlow() + + requestTask + .addOnCompleteListener { + handleOnCompleteRequestTask( + reviewManager = reviewManager, + activity = context.findActivity(), + task = it, + onDismissClick = onDismissClick, + ) + } + .addOnFailureListener(Timber::e) + } + + private fun handleOnCompleteRequestTask( + reviewManager: ReviewManager, + activity: Activity, + task: Task, + onDismissClick: () -> Unit, + ) { + if (task.isSuccessful) { + val reviewFlow = reviewManager.launchReviewFlow(activity, task.result) + reviewFlow + .addOnCompleteListener { resultReviewTask -> + if (!resultReviewTask.isSuccessful) onDismissClick() + } + .addOnFailureListener(Timber::e) + } else { + Timber.e(task.exception) + } + } + + private fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + error("Permissions should be called in the context of an Activity") + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt new file mode 100644 index 0000000000..43a4af298b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import kotlinx.coroutines.flow.FlowCollector +import kotlin.math.abs + +/** + * Flow collector for scroll items tracking. + * If first visible item offset is greater than half item size, then change selected wallet index. + * If last visible item offset is greater than half item size, then change selected wallet index. + * + * @param selectedWalletIndex selected wallet index + * @property lazyListState lazy list state + * @property onWalletChange callback that will be invoked on wallet change + * +[REDACTED_AUTHOR] + */ +internal class ScrollOffsetCollectorV2( + selectedWalletIndex: Int, + private val lazyListState: LazyListState, + private val onWalletChange: (Int) -> Unit, +) : FlowCollector> { + + private val LazyListItemInfo.halfItemSize + get() = size.div(other = 2) + + private var currentIndex = selectedWalletIndex + + override suspend fun emit(value: List) { + if (!lazyListState.isScrollInProgress || value.size <= 1) return + + val firstItem = value.firstOrNull() ?: return + val lastItem = value.lastOrNull() ?: return + + if (abs(firstItem.offset) > firstItem.halfItemSize) { + selectIndex(newIndex = firstItem.index + 1) + } else if (abs(lastItem.offset) > lastItem.halfItemSize) { + selectIndex(newIndex = lastItem.index - 1) + } + } + + private fun selectIndex(newIndex: Int) { + if (currentIndex != newIndex) { + currentIndex = newIndex + onWalletChange(newIndex) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt new file mode 100644 index 0000000000..3dbb6ccb11 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.lazy.LazyListState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val VISIBLE_PART_OF_WALLET_CARD = 0.2f + +internal fun LazyListState.demonstrateScrolling( + coroutineScope: CoroutineScope, + direction: DemonstrateWalletsScrollPreview.Direction, +) { + coroutineScope.launch { + animateScrollBy( + value = calculateOffset(direction = direction, isReverse = false), + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + ) + } + .invokeOnCompletion { + coroutineScope.launch { + animateScrollBy( + value = calculateOffset(direction = direction, isReverse = true), + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + ) + } + } +} + +private fun LazyListState.calculateOffset( + direction: DemonstrateWalletsScrollPreview.Direction, + isReverse: Boolean, +): Float { + val sign = when (direction) { + DemonstrateWalletsScrollPreview.Direction.LEFT -> 1 + DemonstrateWalletsScrollPreview.Direction.RIGHT -> -1 + }.times(other = if (isReverse) -1 else 1) + + return layoutInfo.viewportSize.width.toFloat() * VISIBLE_PART_OF_WALLET_CARD * sign +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index bc1c275b2f..1cb74caecf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -68,6 +68,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory +import com.tangem.feature.wallet.presentation.wallet.subscribers.MaybeTokenListFlow import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -129,10 +130,10 @@ internal class WalletViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler, - hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isNeedToBackupUseCase: IsNeedToBackupUseCase, getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, + hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, // endregion Parameters ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -1352,7 +1353,7 @@ internal class WalletViewModel @Inject constructor( } } - private suspend fun updateButtons(userWallet: UserWallet, currencyStatus: CryptoCurrencyStatus) { + private fun updateButtons(userWallet: UserWallet, currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, @@ -1416,6 +1417,4 @@ internal class WalletViewModel @Inject constructor( } private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver -} - -typealias MaybeTokenListFlow = Flow> \ No newline at end of file +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt new file mode 100644 index 0000000000..a9a2908965 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt @@ -0,0 +1,311 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled +import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview.Direction +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.* +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.properties.Delegates + +@Suppress("LongParameterList") +@HiltViewModel +internal class WalletViewModelV2 @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val walletEventSender: WalletEventSender, + private val walletsUpdateActionResolver: WalletsUpdateActionResolverV2, + private val walletScreenContentLoader: WalletScreenContentLoader, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + analyticsEventsHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : ViewModel() { + + val uiState: StateFlow = stateHolder.uiState + + private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + + private var router: InnerWalletRouter by Delegates.notNull() + private var walletsUpdateJobHolder: JobHolder = JobHolder() + + init { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) + + suggestToEnableBiometrics() + + subscribeOnWalletsUpdateFlow() + subscribeOnBalanceHiding() + subscribeOnSelectedWalletFlow() + } + + fun setWalletRouter(router: InnerWalletRouter) { + this.router = router + clickIntents.initialize(router, viewModelScope) + } + + private fun suggestToEnableBiometrics() { + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { delay(timeMillis = 1_800) } + + if (isShowSaveWalletScreenEnabled()) router.openSaveUserWalletScreen() + } + } + + private suspend fun isShowSaveWalletScreenEnabled(): Boolean { + return router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase() + } + + private fun subscribeOnWalletsUpdateFlow() { + viewModelScope.launch(dispatchers.main) { + shouldSaveUserWalletsUseCase() + .conflate() + .distinctUntilChanged() + .collectLatest { shouldSaveUserWallet -> + getWalletsUseCase() + .distinctUntilChanged() + .conflate() + .map { + walletsUpdateActionResolver.resolve( + wallets = it, + currentState = stateHolder.value, + canSaveWallets = shouldSaveUserWallet, + ) + } + .onEach(::updateWallets) + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(walletsUpdateJobHolder) + } + } + } + + private fun subscribeOnBalanceHiding() { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update(transformer = UpdateBalanceHidingModeTransformer(it.isBalanceHidden)) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun subscribeOnSelectedWalletFlow() { + getSelectedWalletUseCase().onRight { + it + .conflate() + .distinctUntilChanged() + .onEach { selectedWallet -> + if (selectedWallet.isMultiCurrency) { + reduxStateHolder.dispatch( + action = WalletConnectActions.New.Initialize(userWallet = selectedWallet), + ) + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + } + + private fun updateWallets(action: WalletsUpdateActionResolverV2.Action) { + when (action) { + is WalletsUpdateActionResolverV2.Action.InitializeWallets -> initializeWallets(action) + is WalletsUpdateActionResolverV2.Action.ReinitializeWallets -> { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + isRefresh = true, + ) + } + is WalletsUpdateActionResolverV2.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolverV2.Action.AddWallet -> addWallet(action) + is WalletsUpdateActionResolverV2.Action.DeleteWallet -> deleteWallet(action) + is WalletsUpdateActionResolverV2.Action.UnlockWallet -> unlockWallet(action) + is WalletsUpdateActionResolverV2.Action.UpdateWalletCardCount -> { + stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet)) + } + is WalletsUpdateActionResolverV2.Action.UpdateWalletName -> { + stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) + } + is WalletsUpdateActionResolverV2.Action.Unknown -> Unit + } + } + + private fun initializeWallets(action: WalletsUpdateActionResolverV2.Action.InitializeWallets) { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + stateHolder.update( + transformer = InitializeWalletsTransformer( + selectedWalletIndex = action.selectedWalletIndex, + selectedWallet = action.selectedWallet, + wallets = action.wallets, + clickIntents = clickIntents, + ), + ) + + viewModelScope.launch(dispatchers.main) { + if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { + withContext(dispatchers.io) { + delay(timeMillis = 1_800) + } + + walletEventSender.send( + event = WalletEvent.DemonstrateWalletsScrollPreview( + direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { + Direction.RIGHT + } else { + Direction.LEFT + }, + ), + ) + } + } + } + + private fun reinitializeWallet(action: WalletsUpdateActionResolverV2.Action.ReinitializeWallet) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.cancel(action.prevWalletId) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + stateHolder.update( + ReinitializeWalletTransformer(userWallet = action.selectedWallet, clickIntents = clickIntents), + ) + } + } + + private fun addWallet(action: WalletsUpdateActionResolverV2.Action.AddWallet) { + viewModelScope.launch(dispatchers.main) { + stateHolder.update( + AddWalletTransformer( + userWallet = action.selectedWallet, + clickIntents = clickIntents, + ), + ) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + withContext(dispatchers.io) { delay(timeMillis = 700) } + + scrollToWallet(index = action.selectedWalletIndex) + } + } + + private fun deleteWallet(action: WalletsUpdateActionResolverV2.Action.DeleteWallet) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + scrollToWallet(index = action.selectedWalletIndex) + + withContext(dispatchers.io) { delay(timeMillis = 700) } + + stateHolder.update( + DeleteWalletTransformer( + selectedWalletIndex = action.selectedWalletIndex, + deletedWalletId = action.deletedWalletId, + ), + ) + } + } + + private fun unlockWallet(action: WalletsUpdateActionResolverV2.Action.UnlockWallet) { + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { delay(timeMillis = 700) } + + stateHolder.update( + transformer = UnlockWalletTransformer( + unlockedWallets = action.unlockedWallets, + clickIntents = clickIntents, + ), + ) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + } + } + + private fun scrollToWallet(index: Int) { + stateHolder.update( + ScrollToWalletTransformer( + index = index, + currentStateProvider = Provider(action = stateHolder::value), + stateUpdater = { newState -> stateHolder.update { newState } }, + ), + ) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt new file mode 100644 index 0000000000..821148d80c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt @@ -0,0 +1,305 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import dagger.hilt.android.scopes.ViewModelScoped +import timber.log.Timber +import javax.inject.Inject + +/** + * Resolver that determines which update action will be performed + * + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + */ +@ViewModelScoped +internal class WalletsUpdateActionResolverV2 @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) { + + private var isInitialized: Boolean = false + private var canSaveWallets: Boolean = false + + fun resolve(wallets: List, currentState: WalletScreenState, canSaveWallets: Boolean): Action { + val selectedWallet = wallets.getSelectedWallet() ?: return Action.Unknown + + val action = when { + isFirstInitialization(currentState) -> { + createInitializeWalletsAction(wallets, selectedWallet, canSaveWallets) + } + isReinitialization(canSaveWallets) -> { + this.canSaveWallets = canSaveWallets + Action.ReinitializeWallets(selectedWallet = selectedWallet) + } + else -> getUpdateContentAction(currentState, wallets, selectedWallet) + } + + Timber.d("Resolved action: $action") + + return action + } + + private fun List.getSelectedWallet(): UserWallet? { + return when { + isEmpty() -> null + size == 1 -> first() + else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it }) + } + } + + private fun isFirstInitialization(state: WalletScreenState): Boolean { + return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX + } + + private fun createInitializeWalletsAction( + wallets: List, + selectedWallet: UserWallet, + canSaveWallets: Boolean, + ): Action { + this.isInitialized = true + this.canSaveWallets = canSaveWallets + + return Action.InitializeWallets( + selectedWalletIndex = wallets.indexOfWallet(selectedWallet.walletId), + selectedWallet = selectedWallet, + wallets = wallets, + ) + } + + private fun isReinitialization(canSaveWallets: Boolean): Boolean { + return isInitialized && this.canSaveWallets != canSaveWallets + } + + private fun getUpdateContentAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return when { + isWalletsCountChanged(state, wallets) -> { + getChangeWalletsListAction(state, wallets, selectedWallet) + } + isAnotherWalletSelected(state, selectedWallet) -> { + Action.ReinitializeWallet( + prevWalletId = state.getPrevSelectedWallet().id, + selectedWallet = selectedWallet, + ) + } + else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) + } + } + + private fun isWalletsCountChanged(state: WalletScreenState, wallets: List): Boolean { + val prevWalletsSize = state.wallets.size + val walletsSize = wallets.size + + return prevWalletsSize != walletsSize + } + + private fun getChangeWalletsListAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val prevWalletsSize = state.wallets.size + + return when { + prevWalletsSize > wallets.size -> { + Action.DeleteWallet( + selectedWallet = selectedWallet, + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + deletedWalletId = state.wallets.getDeletedWalletId(wallets), + ) + } + prevWalletsSize < wallets.size -> { + val newUserWallet = state.wallets.getAddedWallet(wallets) + Action.AddWallet( + selectedWalletIndex = wallets.indexOfWallet(id = newUserWallet.walletId), + selectedWallet = newUserWallet, + ) + } + else -> error("Wallets list is not changed") + } + } + + private fun List.getDeletedWalletId(wallets: List): UserWalletId { + return this + .map { it.walletCardState.id } + .firstOrNull { !wallets.map(UserWallet::walletId).contains(it) } + ?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun List.getAddedWallet(wallets: List): UserWallet { + return wallets + .firstOrNull { wallet -> !this.map { it.walletCardState.id }.contains(wallet.walletId) } + ?: error("Added wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun isAnotherWalletSelected(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.getPrevSelectedWallet().id != selectedWallet.walletId + } + + private fun getUpdateSelectedWalletAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return when { + isSelectedWalletNameChanged(state, selectedWallet) -> { + Action.UpdateWalletName(selectedWalletId = selectedWallet.walletId, name = selectedWallet.name) + } + isSelectedWalletUnlocked(state, selectedWallet) -> { + Action.UnlockWallet( + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet) + else -> Action.Unknown + } + } + + private fun isSelectedWalletNameChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.getPrevSelectedWallet().title != selectedWallet.name + } + + private fun isSelectedWalletUnlocked(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.isSelectedWalletLocked() && !selectedWallet.isLocked + } + + private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + val prevSelectedWallet = state.getPrevSelectedWallet() + return prevSelectedWallet is WalletCardState.Content && + prevSelectedWallet.cardCount != selectedWallet.getCardsCount() + } + + private fun WalletScreenState.isSelectedWalletLocked(): Boolean { + val selectedWalletState = wallets.getOrNull(selectedWalletIndex) ?: error("Selected wallet is not found") + return selectedWalletState is WalletState.MultiCurrency.Locked || + selectedWalletState is WalletState.SingleCurrency.Locked + } + + private fun WalletScreenState.getPrevSelectedWallet(): WalletCardState { + return wallets + .map(WalletState::walletCardState) + .getOrNull(selectedWalletIndex) + ?: error("Previous selected wallet is not found") + } + + private fun List.indexOfWallet(id: UserWalletId): Int { + val selectedIndex = indexOfFirst { it.walletId == id } + + return if (selectedIndex == -1) { + error("Wallets don't contain a wallet with id: $id") + } else { + selectedIndex + } + } + + sealed class Action { + + data class InitializeWallets( + val selectedWalletIndex: Int, + val selectedWallet: UserWallet, + val wallets: List, + ) : Action() { + + override fun toString(): String { + return """ + Initialize( + selectedWalletIndex=$selectedWalletIndex, + selectedWallet=${selectedWallet.walletId}, + wallets=${wallets.joinToString { it.walletId.toString() }} + ) + """.trimIndent() + } + } + + /** + * Reinitialize wallets. Example, if user turned on wallets saving + * + * @property selectedWallet selected wallet + */ + data class ReinitializeWallets(val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "Reinitialize(selectedWallet=${selectedWallet.walletId})" + } + } + + /** + * Reinitialize selected wallet. Example, scanning a new card if wallets saving is turned off + * + * @property prevWalletId previous selected wallet id + * @property selectedWallet selected wallet + */ + data class ReinitializeWallet(val prevWalletId: UserWalletId, val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "ReinitializeWallet(prevWalletId=$prevWalletId, selectedWallet=${selectedWallet.walletId})" + } + } + + data class UpdateWalletName(val selectedWalletId: UserWalletId, val name: String) : Action() { + + override fun toString(): String { + return "UpdateWalletName(selectedWalletId=$selectedWalletId, name=$name)" + } + } + + data class UnlockWallet(val selectedWallet: UserWallet, val unlockedWallets: List) : Action() { + + override fun toString(): String { + return """ + UnlockWallet( + selectedWallet=${selectedWallet.walletId}, + unlockedWallets=${unlockedWallets.joinToString { it.walletId.toString() }} + ) + """.trimIndent() + } + } + + data class DeleteWallet( + val selectedWallet: UserWallet, + val selectedWalletIndex: Int, + val deletedWalletId: UserWalletId, + ) : Action() { + + override fun toString(): String { + return """ + DeleteWallet( + selectedWallet=${selectedWallet.walletId}, + selectedWalletIndex=$selectedWalletIndex, + deletedWalletId=$deletedWalletId + ) + """.trimIndent() + } + } + + data class AddWallet(val selectedWalletIndex: Int, val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "AddWallet(selectedWalletIndex=$selectedWalletIndex, selectedWallet=${selectedWallet.walletId})" + } + } + + /** + * Update wallet card count. Example, if user backed up wallet + * + * @property selectedWallet selected wallet + */ + data class UpdateWalletCardCount(val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "UpdateWalletCardCount(selectedWallet=${selectedWallet.walletId})" + } + } + + object Unknown : Action() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt new file mode 100644 index 0000000000..d9be6984f5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import kotlinx.coroutines.CoroutineScope +import kotlin.properties.Delegates + +/** + * Base wallet click intents component. + * Provides router and viewModelScope to child classes. + * +[REDACTED_AUTHOR] + */ +@Suppress("UnnecessaryAbstractClass") +internal abstract class BaseWalletClickIntents { + + protected val router: InnerWalletRouter get() = _router + protected val viewModelScope: CoroutineScope get() = _viewModelScope + + private var _router: InnerWalletRouter by Delegates.notNull() + private var _viewModelScope: CoroutineScope by Delegates.notNull() + + open fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + _router = router + _viewModelScope = coroutineScope + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt new file mode 100644 index 0000000000..1bcff88b81 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -0,0 +1,72 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.core.navigation.AppScreen +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal interface WalletCardClickIntents { + + fun onRenameClick(userWalletId: UserWalletId, name: String) + + fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) + + fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) +} + +internal class WalletCardClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val walletScreenContentLoader: WalletScreenContentLoader, + private val updateWalletUseCase: UpdateWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), WalletCardClickIntents { + + override fun onRenameClick(userWalletId: UserWalletId, name: String) { + viewModelScope.launch(dispatchers.main) { + updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name) }) + } + } + + override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.RemoveWalletAlert( + onConfirmClick = { onDeleteAfterConfirmationClick(userWalletId) }, + ), + ), + ) + } + + override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.cancel(userWalletId) + deleteWalletUseCase(userWalletId) + .onRight { popBackIfAllWalletsIsLocked() } + .onLeft { Timber.e(it.toString()) } + } + } + + private fun popBackIfAllWalletsIsLocked() { + val wallets = stateHolder.value.wallets.map(WalletState::walletCardState) + val unlockedWallet = wallets.count { it !is WalletCardState.LockedContent } + + if (unlockedWallet == 1) { + router.popBackStack( + screen = if (wallets.size > 1) AppScreen.Welcome else AppScreen.Home, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt new file mode 100644 index 0000000000..55e0a76bc5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -0,0 +1,149 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.settings.NeverToShowWalletsScrollPreview +import com.tangem.domain.tokens.FetchCardTokenListUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetRefreshStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ViewModelScoped +internal class WalletClickIntentsV2 @Inject constructor( + private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, + private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, + private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, + private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, + private val stateHolder: WalletStateHolderV2, + private val walletScreenContentLoader: WalletScreenContentLoader, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val selectWalletUseCase: SelectWalletUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), + WalletCardClickIntents by walletCardClickIntentsImplementor, + WalletWarningsClickIntents by warningsClickIntentsImplementer, + WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, + WalletContentClickIntents by contentClickIntentsImplementor { + + override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + super.initialize(router, coroutineScope) + + walletCardClickIntentsImplementor.initialize(router, coroutineScope) + warningsClickIntentsImplementer.initialize(router, coroutineScope) + currencyActionsClickIntentsImplementor.initialize(router, coroutineScope) + contentClickIntentsImplementor.initialize(router, coroutineScope) + } + + fun onWalletChange(index: Int) { + viewModelScope.launch(dispatchers.main) { + launch(dispatchers.main) { neverToShowWalletsScrollPreview() } + + val maybeUserWallet = selectWalletUseCase( + userWalletId = stateHolder.value.wallets[index].walletCardState.id, + ) + + stateHolder.update { it.copy(selectedWalletIndex = index) } + + maybeUserWallet.onRight { + walletScreenContentLoader.load( + userWallet = it, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + clickIntents = this@WalletClickIntentsV2, + coroutineScope = viewModelScope, + ) + } + } + } + + fun onRefreshSwipe() { + when (stateHolder.getSelectedWallet()) { + is WalletState.MultiCurrency.Content -> { + analyticsEventHandler.send(PortfolioEvent.Refreshed) + refreshMultiCurrencyContent() + } + is WalletState.SingleCurrency.Content -> { + analyticsEventHandler.send(PortfolioEvent.Refreshed) + refreshSingleCurrencyContent() + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> Unit + } + } + + private fun refreshMultiCurrencyContent() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + ) + + viewModelScope.launch(dispatchers.main) { + val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } else { + fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } + + maybeFetchResult.onLeft { + stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it)) + } + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), + ) + } + } + + fun onReloadClick() { + refreshSingleCurrencyContent() + } + + // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary + // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() + private fun refreshSingleCurrencyContent() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + ) + + viewModelScope.launch(dispatchers.main) { + fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) + + walletScreenContentLoader.load( + userWallet = userWallet, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + clickIntents = this@WalletClickIntentsV2, + coroutineScope = viewModelScope, + isRefresh = true, + ) + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt new file mode 100644 index 0000000000..a3582c6e63 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -0,0 +1,123 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCurrencyActionsConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal interface WalletContentClickIntents { + + fun onBackClick() + + fun onDetailsClick() + + fun onManageTokensClick() + + fun onOrganizeTokensClick() + + fun onTokenItemClick(currency: CryptoCurrency) + + fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onTransactionClick(txHash: String) +} + +@Suppress("LongParameterList") +@ViewModelScoped +internal class WalletContentClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : BaseWalletClickIntents(), WalletContentClickIntents { + + override fun onBackClick() = router.popBackStack() + + override fun onDetailsClick() = router.openDetailsScreen() + + override fun onManageTokensClick() { + analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) + reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) + router.openManageTokensScreen() + } + + override fun onOrganizeTokensClick() { + analyticsEventHandler.send(PortfolioEvent.OrganizeTokens) + router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) + } + + override fun onTokenItemClick(currency: CryptoCurrency) { + analyticsEventHandler.send(PortfolioEvent.TokenTapped) + router.openTokenDetails(stateHolder.getSelectedWalletId(), currency) + } + + override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + viewModelScope.launch(dispatchers.main) { + getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) + .take(count = 1) + .collectLatest { + showActionsBottomSheet(it, userWallet) + } + } + } + + private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWallet.walletId, + content = ActionsBottomSheetConfig( + actions = MultiWalletCurrencyActionsConverter( + userWallet = userWallet, + clickIntents = currencyActionsClickIntentsImplementor, + ).convert(tokenActionsState), + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWallet.walletId), + ) + }, + ), + ) + } + + override fun onTransactionClick(txHash: String) { + viewModelScope.launch(dispatchers.main) { + val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap( + userWalletId = stateHolder.getSelectedWalletId(), + ) + ?.currency + ?: return@launch + + router.openUrl( + url = getExplorerTransactionUrlUseCase(txHash = txHash, networkId = currency.network.id), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt new file mode 100644 index 0000000000..9690fa7684 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -0,0 +1,418 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase +import com.tangem.domain.tokens.RemoveCurrencyUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import javax.inject.Inject + +interface WalletCurrencyActionsClickIntents { + + fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onExploreClick() +} + +@Suppress("LongParameterList", "LargeClass") +@ViewModelScoped +internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val walletManagersFacade: WalletManagersFacade, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { + + override fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrencyStatus.currency.symbol), + ) + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) + + when (val currency = cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet) + is CryptoCurrency.Token -> sendToken(currency, cryptoCurrencyStatus.value, userWallet) + } + } + + private fun sendCoin(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin(userWallet = userWallet, coinStatus = cryptoCurrencyStatus), + ) + } + + private fun sendToken( + cryptoCurrency: CryptoCurrency.Token, + cryptoCurrencyStatus: CryptoCurrencyStatus.Status, + userWallet: UserWallet, + ) { + viewModelScope.launch(dispatchers.main) { + getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = userWallet, + tokenCurrency = cryptoCurrency, + tokenFiatRate = cryptoCurrencyStatus.fiatRate, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } + } + + override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWalletId = stateHolder.getSelectedWalletId() + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + val currency = cryptoCurrencyStatus.currency + val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + + analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = createReceiveBottomSheetContent(currency, addresses), + onDismissBottomSheet = { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + }, + ), + ) + } + } + + private fun createReceiveBottomSheetContent( + currency: CryptoCurrency, + addresses: List
, + ): TangemBottomSheetConfigContent { + return TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = addresses.map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + }, + onCopyClick = { + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + }, + onShareClick = { + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + }, + ) + } + + override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonCopyAddress(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + walletManagersFacade.getAddress( + userWalletId = stateHolder.getSelectedWalletId(), + network = cryptoCurrencyStatus.currency.network, + ) + .find { it.type == AddressType.Default } + ?.value + ?.let { + walletEventSender.send( + event = WalletEvent.CopyAddress( + address = it, + toast = resourceReference(R.string.wallet_notification_address_copied), + ), + ) + } + } + } + + override fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = getHideTokeAlertConfig(stateHolder.getSelectedWalletId(), cryptoCurrencyStatus), + ), + ) + } + } + + private suspend fun getHideTokeAlertConfig( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): WalletAlertState.DefaultAlert { + val currency = cryptoCurrencyStatus.currency + return if (currency is CryptoCurrency.Coin && !isCryptoCurrencyCoinCouldHide(userWalletId, currency)) { + WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = WrappedList( + listOf( + cryptoCurrencyStatus.currency.name, + cryptoCurrencyStatus.currency.network.name, + ), + ), + ), + onConfirmClick = null, + ) + } else { + WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + onConfirmClick = { onPerformHideToken(cryptoCurrencyStatus) }, + ) + } + } + + override fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWalletId = stateHolder.getSelectedWalletId() + + viewModelScope.launch(dispatchers.io) { + removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) + .fold( + ifLeft = { + walletEventSender.send( + event = WalletEvent.ShowToast(text = resourceReference(R.string.common_error)), + ) + }, + ifRight = { + getSelectedWalletSyncUseCase.unwrap()?.let { userWallet -> + reduxStateHolder.dispatch( + action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet), + ) + } + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + }, + ) + } + } + + override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrencyStatus.currency.symbol), + ) + + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.Sell( + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } + } + } + + override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrencyStatus.currency.symbol), + ) + + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } + } + } + + override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), + ) + + reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) + } + + override fun onExploreClick() { + showErrorIfDemoModeOrElse(action = ::openExplorer) + } + + private fun openExplorer() { + val userWalletId = stateHolder.getSelectedWalletId() + + viewModelScope.launch(dispatchers.main) { + val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch + val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + + if (addresses.size == 1) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.Default, + ), + ) + } else { + showChooseAddressBottomSheet(userWalletId, addresses, currency) + } + } + } + + private fun showChooseAddressBottomSheet( + userWalletId: UserWalletId, + addresses: List
, + currency: CryptoCurrency, + ) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = ChooseAddressBottomSheetConfig( + addressModels = addresses + .map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + } + .toImmutableList(), + onClick = { + onAddressTypeSelected( + userWalletId = userWalletId, + currency = currency, + addressModel = it, + ) + }, + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + }, + ), + ) + } + + private fun onAddressTypeSelected( + userWalletId: UserWalletId, + currency: CryptoCurrency, + addressModel: AddressModel, + ) { + viewModelScope.launch(dispatchers.main) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.valueOf(addressModel.type.name), + ), + ) + + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + } + } + + private fun showErrorIfDemoModeOrElse(action: () -> Unit) { + val cardId = getSelectedWalletSyncUseCase.unwrap()?.cardId ?: return + + if (isDemoCardUseCase(cardId = cardId)) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + + walletEventSender.send( + event = WalletEvent.ShowError( + text = resourceReference(id = R.string.alert_demo_feature_disabled), + ), + ) + } else { + action() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt new file mode 100644 index 0000000000..cfa8be01ab --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -0,0 +1,287 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.blockchain.blockchains.cardano.CardanoUtils +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.card.SetCardWasScannedUseCase +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.NeverToSuggestRateAppUseCase +import com.tangem.domain.settings.RemindToRateAppLaterUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UnlockWalletsError +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler +import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletError +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal interface WalletWarningsClickIntents { + + fun onAddBackupCardClick() + + fun onCloseAlreadySignedHashesWarningClick() + + fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) + + fun onOpenUnlockWalletsBottomSheetClick() + + fun onUnlockWalletClick() + + fun onScanToUnlockWalletClick() + + fun onLikeAppClick() + + fun onDislikeAppClick() + + fun onCloseRateAppWarningClick() +} + +@Suppress("LongParameterList") +@ViewModelScoped +internal class WalletWarningsClickIntentsImplementer @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val unlockWalletsUseCase: UnlockWalletsUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val setCardWasScannedUseCase: SetCardWasScannedUseCase, + private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, + private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val reduxStateHolder: ReduxStateHolder, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), WalletWarningsClickIntents { + + override fun onAddBackupCardClick() { + analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped) + router.openOnboardingScreen() + } + + override fun onCloseAlreadySignedHashesWarningClick() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + viewModelScope.launch(dispatchers.main) { + setCardWasScannedUseCase(cardId = userWallet.cardId) + } + } + + override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)) + analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) + + viewModelScope.launch(dispatchers.main) { + deriveMissingCurrencies( + scanResponse = userWallet.scanResponse, + currencyList = missedAddressCurrencies, + ) { scannedCardResponse -> + updateWalletUseCase( + userWalletId = userWallet.walletId, + update = { it.copy(scanResponse = scannedCardResponse) }, + ) + .onRight { fetchTokenListUseCase(userWalletId = it.walletId) } + } + } + } + + // TODO: [REDACTED_JIRA] + private fun deriveMissingCurrencies( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: suspend (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value))?.let { curve -> + getNewDerivations(curve, scanResponse, it) + } + } + + val derivations = buildMap> { + derivationDataList.forEach { + val current = this[it.derivations.first] + if (current != null) { + current.addAll(it.derivations.second) + current.distinct() + } else { + this[it.derivations.first] = it.derivations.second.toMutableList() + } + } + }.ifEmpty { return } + + viewModelScope.launch(dispatchers.io) { + derivePublicKeysUseCase(cardId = null, derivations = derivations) + .onRight { + val newDerivedKeys = it.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) + + onSuccess(updatedScanResponse) + } + } + } + + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currency: CryptoCurrency, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val blockchain = Blockchain.fromId(currency.network.id.value) + val supportedCurves = blockchain.getSupportedCurves() + val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) + .takeIf { supportedCurves.contains(curve) } + + val customPath = currency.network.derivationPath.value?.let { + DerivationPath(it) + }.takeIf { supportedCurves.contains(curve) } + + val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) { + currency.network.derivationPath.value?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + + class DerivationData(val derivations: Pair>) + + override fun onOpenUnlockWalletsBottomSheetClick() { + stateHolder.update( + OpenBottomSheetTransformer( + content = requireNotNull(stateHolder.getSelectedWallet().bottomSheetConfig).content, + userWalletId = stateHolder.getSelectedWalletId(), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()), + ) + }, + ), + ) + } + + override fun onUnlockWalletClick() { + analyticsEventHandler.send(MainScreen.NoticeWalletLocked) + + viewModelScope.launch(dispatchers.main) { + unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true) + .onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) } + .onLeft(::handleUnlockWalletsError) + } + } + + private fun handleUnlockWalletsError(error: UnlockWalletsError) { + val event = when (error) { + is UnlockWalletsError.DataError, + is UnlockWalletsError.UnableToUnlockWallets, + -> WalletEvent.ShowToast(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) + is UnlockWalletsError.NoUserWalletSelected, + is UnlockWalletsError.NotAllUserWalletsUnlocked, + -> WalletEvent.ShowAlert(WalletAlertState.RescanWallets) + } + + walletEventSender.send(event) + } + + override fun onScanToUnlockWalletClick() { + analyticsEventHandler.send(event = MainScreen.WalletUnlockTapped) + + viewModelScope.launch(dispatchers.main) { + scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) + .onLeft { error -> + when (error) { + ScanCardToUnlockWalletError.WrongCardIsScanned -> { + walletEventSender.send( + event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), + ) + } + ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog() + } + } + } + } + + override fun onLikeAppClick() { + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) + + walletEventSender.send( + event = WalletEvent.RateApp( + onDismissClick = { + viewModelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + } + }, + ), + ) + } + + override fun onDislikeAppClick() { + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) + + viewModelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + + reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter) + } + } + + override fun onCloseRateAppWarningClick() { + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed)) + + viewModelScope.launch(dispatchers.main) { + remindToRateAppLaterUseCase() + } + } +} \ No newline at end of file