Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-29 20:13:09 +03:00
commit bb28c0a7e3
100 changed files with 6093 additions and 46 deletions

View file

@ -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)
}
}

View file

@ -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<CryptoCurrency>,
) {
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())

View file

@ -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<Preferences> */

View file

@ -34,5 +34,9 @@
{
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "undefined"
},
{
"name": "WALLETS_SCROLLING_PREVIEW_ENABLED",
"version": "5.4.0"
}
]

View file

@ -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 <A> EventEffect(event: StateEvent<A>, onTrigger: suspend (data: A) -> Unit) {
LaunchedEffect(event) {
if (event is StateEvent.Triggered<A>) {
onTrigger(event.data)
event.onConsume()
launch { onTrigger(event.data) }
.invokeOnCompletion { event.onConsume() }
}
}
}

View file

@ -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<Boolean> {
return appPreferencesStore.getObject<List<UsedCardInfo>>(key = PreferencesKeys.USED_CARDS_INFO_KEY)
return appPreferencesStore.getObjectList<UsedCardInfo>(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<UsedCardInfo>? = mutablePreferences.getObject(
val usedCards: List<UsedCardInfo>? = 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,
)

View file

@ -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,
)
}
}

View file

@ -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,
)
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -32,10 +32,7 @@ class GetCryptoCurrencyActionsUseCase(
) {
@OptIn(ExperimentalCoroutinesApi::class)
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Flow<TokenActionsState> {
operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow<TokenActionsState> {
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)
}

View file

@ -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<SelectWalletError, Unit> {
suspend operator fun invoke(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> {
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()
}
}
}
}
}

View file

@ -9,4 +9,6 @@ interface WalletFeatureToggles {
/** Availability of redesigned screen */
val isRedesignedScreenEnabled: Boolean
val isWalletsScrollingPreviewEnabled: Boolean
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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<WalletViewModel>().apply { router = this@DefaultWalletRouter }
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) {
val viewModel = hiltViewModel<WalletViewModel>().apply {
router = this@DefaultWalletRouter
}
WalletScreen(state = viewModel.uiState)
WalletScreen(state = viewModel.uiState)
} else {
val viewModel = hiltViewModel<WalletViewModelV2>().apply {
setWalletRouter(router = this@DefaultWalletRouter)
}
WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value)
}
}
composable(

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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<CurrencyStatusError, CryptoCurrencyStatus>::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) }
}
}

View file

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

View file

@ -0,0 +1,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
}
}
}

View file

@ -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<UserWalletId, List<Job>>()
fun contains(id: UserWalletId) = loaders.containsKey(id)
fun set(id: UserWalletId, jobs: List<Job>) {
loaders[id] = jobs
}
fun remove(id: UserWalletId) {
loaders[id]?.let {
it.forEach(Job::cancel)
loaders.remove(id)
}
}
}

View file

@ -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) }
}
}

View file

@ -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<WalletSubscriber<*>> {
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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<WalletSubscriber<*>> {
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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<WalletSubscriber<*>> {
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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<WalletSubscriber<*>> get() = create()
protected abstract fun create(): List<WalletSubscriber<*>>
}

View file

@ -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,
}
}
}

View file

@ -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<WalletState>,
val onWalletChange: (Int) -> Unit,
val event: StateEvent<WalletEvent>,
val isHidingMode: Boolean,
)
internal sealed class WalletState {
abstract val pullToRefreshConfig: WalletPullToRefreshConfig
abstract val walletCardState: WalletCardState
abstract val warnings: ImmutableList<WalletNotification>
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<WalletNotification>,
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<WalletNotification> = 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<WalletManageButton>
abstract val marketPriceBlockState: MarketPriceBlockState?
abstract val txHistoryState: TxHistoryState
data class Content(
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val walletCardState: WalletCardState,
override val warnings: ImmutableList<WalletNotification>,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val buttons: PersistentList<WalletManageButton>,
override val marketPriceBlockState: MarketPriceBlockState,
override val txHistoryState: TxHistoryState,
) : SingleCurrency()
data class Locked(
override val walletCardState: WalletCardState,
override val buttons: PersistentList<WalletManageButton>,
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<WalletNotification> = 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<TokensListItemState>
abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig?
object Loading : ContentState() {
override val items = persistentListOf<TokensListItemState>()
override val organizeTokensButtonConfig = null
}
data class Content(
override val items: ImmutableList<TokensListItemState>,
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)

View file

@ -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<WalletScreenState> get() = mutableUiState
val value: WalletScreenState get() = uiState.value
private val mutableUiState: MutableStateFlow<WalletScreenState> = 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,
)
}
}

View file

@ -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(),
)
}
}

View file

@ -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)
}
}
}

View file

@ -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 }
}
}

View file

@ -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<UserWallet>,
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<WalletManageButton> {
return persistentListOf(
WalletManageButton.Buy(enabled = false, onClick = {}),
WalletManageButton.Send(enabled = false, onClick = {}),
WalletManageButton.Receive(enabled = false, onClick = {}),
WalletManageButton.Sell(enabled = false, onClick = {}),
)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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),
),
)
}
}

View file

@ -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
}
}
}
}

View file

@ -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<WalletScreenState>,
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(),
),
)
},
),
)
}
}

View file

@ -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),
)
}
}

View file

@ -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<WalletManageButton> {
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<TokenActionsState.ActionState>.filterIfS2C(): List<TokenActionsState.ActionState> {
return if (userWallet.scanResponse.cardTypesResolver.isStart2Coin()) {
filterNot { it is TokenActionsState.ActionState.Buy || it is TokenActionsState.ActionState.Sell }
} else {
this
}
}
}

View file

@ -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)
}
}

View file

@ -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<WalletManageButton>.toUpdatedState(): PersistentList<WalletManageButton> {
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
}
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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<TxHistoryItem>,
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,
)
}
},
)
}
}

View file

@ -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<TxHistoryState.TxHistoryItemState> {
return buildList {
add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick))
(1..transactionsCount).forEach {
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString())))
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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<PagingData<TxHistoryItem>>,
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
}
}
}
}

View file

@ -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<WalletNotification>,
) : 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
}
}
}
}

View file

@ -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<UserWallet>,
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
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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(),
)
}
}

View file

@ -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<WalletCardState, WalletCardState> {
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(),
)
}
}

View file

@ -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<TokenActionsState, ImmutableList<TokenActionButtonConfig>> {
override fun convert(value: TokenActionsState): ImmutableList<TokenActionButtonConfig> {
return value.states
.filterIfSingleWithToken()
.mapNotNull {
mapTokenActionState(actionsState = it, cryptoCurrencyStatus = value.cryptoCurrencyStatus)
}
.toImmutableList()
}
private fun List<TokenActionsState.ActionState>.filterIfSingleWithToken(): List<TokenActionsState.ActionState> {
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,
)
}
}

View file

@ -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<WalletCardState, WalletCardState> {
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,
)
}
}

View file

@ -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<MarketPriceBlockState, MarketPriceBlockState> {
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
}
}

View file

@ -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<AppCurrency>,
private val clickIntents: WalletClickIntentsV2,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
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
}
}

View file

@ -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<WalletTokensListState, WalletTokensListState> {
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<TokensListItemState> {
return groups.fold(initial = persistentListOf()) { acc, group ->
acc.mutate { it.addGroup(group) }
}
}
private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList<TokensListItemState> {
return currencies.fold(initial = persistentListOf()) { acc, token ->
acc.mutate { it.addToken(token) }
}
}
private fun MutableList<TokensListItemState>.addGroup(group: NetworkGroup): List<TokensListItemState> {
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<TokensListItemState>.addToken(token: CryptoCurrencyStatus): List<TokensListItemState> {
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()
}
}

View file

@ -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<Flow<PagingData<TxHistoryItem>>, 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<PagingData<TxHistoryItem>>): 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<TxHistoryItem, TxHistoryItemState> { 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<TxHistoryItemState>.insertGroupTitle(): PagingData<TxHistoryItemState> {
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<TxHistoryItemState>.formatTransactionsTimestamp(): PagingData<TxHistoryItemState> {
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
}
}
}

View file

@ -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<TxHistoryItem, TransactionState> {
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)
}
}

View file

@ -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()
}

View file

@ -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())
}
}
}

View file

@ -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<WalletManageButton> {
return persistentListOf(
WalletManageButton.Buy(enabled = false, onClick = {}),
WalletManageButton.Send(enabled = false, onClick = {}),
WalletManageButton.Receive(enabled = false, onClick = {}),
WalletManageButton.Sell(enabled = false, onClick = {}),
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -66,6 +66,7 @@ internal fun WalletEventEffect(
}
.addOnFailureListener(Timber::e)
}
is WalletEvent.DemonstrateWalletsScrollPreview -> Unit
}
},
)

View file

@ -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<WalletEvent>,
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)
}
}
},
)
}

View file

@ -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<WalletAlertState?>(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<Boolean>,
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,
)
}
}
}
}

View file

@ -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<Boolean>,
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),
)
}
}

View file

@ -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<WalletCardState>,
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

View file

@ -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<TxHistoryState.TxHistoryItemState>?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is WalletStateV2.MultiCurrency -> {
tokensListItemsV2(state.tokensListState, modifier, isBalanceHidden)
}
is WalletStateV2.SingleCurrency -> {
txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)
}
}
}

View file

@ -22,7 +22,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
items(
items = configs,
key = { it::class.java },
contentType = { it.config::class.java },
contentType = { it::class.java },
itemContent = {
Notification(
config = it.config,

View file

@ -19,6 +19,8 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
import kotlinx.collections.immutable.ImmutableList
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState as WalletTokensListStateV2
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState as TokensListItemStateV2
private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
@ -45,6 +47,23 @@ internal fun LazyListScope.tokensListItems(
}
}
internal fun LazyListScope.tokensListItemsV2(
state: WalletTokensListStateV2,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
) {
when (state) {
is WalletTokensListStateV2.ContentState -> {
contentItemsV2(
items = state.items,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
WalletTokensListStateV2.Empty -> nonContentItem(modifier = modifier)
}
}
private fun LazyListScope.contentItems(
items: ImmutableList<WalletTokensListState.TokensListItemState>,
modifier: Modifier = Modifier,
@ -67,6 +86,28 @@ private fun LazyListScope.contentItems(
)
}
private fun LazyListScope.contentItemsV2(
items: ImmutableList<TokensListItemStateV2>,
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(

View file

@ -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)
}
}
}

View file

@ -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()
}

View file

@ -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<ReviewInfo>,
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")
}
}

View file

@ -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<List<LazyListItemInfo>> {
private val LazyListItemInfo.halfItemSize
get() = size.div(other = 2)
private var currentIndex = selectedWalletIndex
override suspend fun emit(value: List<LazyListItemInfo>) {
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)
}
}
}

View file

@ -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
}

View file

@ -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<Either<TokenListError, TokenList>>
}

View file

@ -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<WalletScreenState> = 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<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
}
}

View file

@ -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<UserWallet>, 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<UserWallet>.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<UserWallet>,
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<UserWallet>,
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<UserWallet>): Boolean {
val prevWalletsSize = state.wallets.size
val walletsSize = wallets.size
return prevWalletsSize != walletsSize
}
private fun getChangeWalletsListAction(
state: WalletScreenState,
wallets: List<UserWallet>,
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<WalletState>.getDeletedWalletId(wallets: List<UserWallet>): 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<WalletState>.getAddedWallet(wallets: List<UserWallet>): 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<UserWallet>,
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<UserWallet>.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<UserWallet>,
) : 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<UserWallet>) : 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()
}
}

View file

@ -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
}
}

View file

@ -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,
)
}
}
}

View file

@ -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),
)
}
}
}

View file

@ -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),
)
}
}
}

View file

@ -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<Address>,
): 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<Address>,
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()
}
}
}

View file

@ -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<CryptoCurrency>)
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<CryptoCurrency>) {
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<CryptoCurrency>,
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<ByteArrayKey, MutableList<DerivationPath>> {
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<ByteArrayKey, List<DerivationPath>>)
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()
}
}
}