Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-27 14:58:11 +08:00
parent aabfc1bba1
commit 422afbc17f
7 changed files with 934 additions and 44 deletions

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

@ -1,11 +1,20 @@
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.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
/**
[REDACTED_AUTHOR]
*/
internal interface WalletCardClickIntents {
fun onRenameClick(userWalletId: UserWalletId, name: String)
@ -15,17 +24,48 @@ internal interface WalletCardClickIntents {
fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId)
}
internal class WalletCardClickIntentsImplementor @Inject constructor() : WalletCardClickIntents {
internal class WalletCardClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateHolderV2,
private val walletEventSender: WalletEventSender,
// TODO: 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) {
// TODO
viewModelScope.launch(dispatchers.main) {
updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name) })
}
}
override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) {
// TODO
walletEventSender.send(
event = WalletEvent.ShowAlert(
state = WalletAlertState.RemoveWalletAlert(
onConfirmClick = { onDeleteAfterConfirmationClick(userWalletId) },
),
),
)
}
override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) {
// TODO
viewModelScope.launch(dispatchers.main) {
// TODO: 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

@ -1,27 +1,150 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class WalletClickIntentsV2 @Inject constructor(
private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor,
private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer,
private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor,
private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor,
) : WalletCardClickIntents by walletCardClickIntentsImplementor,
private val stateHolder: WalletStateHolderV2,
// TODO: private val walletScreenContentLoader: WalletScreenContentLoader,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val selectWalletUseCase: SelectWalletUseCase,
// TODO: 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 {
@Suppress("UnusedPrivateMember")
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) {
// TODO
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 {
// TODO:
// walletScreenContentLoader.load(
// userWallet = it,
// appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
// clickIntents = this@WalletClickIntentsV2,
// coroutineScope = viewModelScope,
// )
}
}
}
fun onRefreshSwipe() {
// TODO
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() {
// TODO
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)
// TODO:
// walletScreenContentLoader.load(
// userWallet = userWallet,
// appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
// clickIntents = this@WalletClickIntentsV2,
// coroutineScope = viewModelScope,
// isRefresh = true,
// )
stateHolder.update(
SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false),
)
}
}
}

View file

@ -1,7 +1,27 @@
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 kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import javax.inject.Inject
internal interface WalletContentClickIntents {
@ -21,33 +41,81 @@ internal interface WalletContentClickIntents {
fun onTransactionClick(txHash: String)
}
internal class WalletContentClickIntentsImplementor @Inject constructor() : WalletContentClickIntents {
@Suppress("LongParameterList")
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() {
// TODO
}
override fun onBackClick() = router.popBackStack()
override fun onDetailsClick() {
// TODO
}
override fun onDetailsClick() = router.openDetailsScreen()
override fun onManageTokensClick() {
// TODO
analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens)
reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess)
router.openManageTokensScreen()
}
override fun onOrganizeTokensClick() {
// TODO
analyticsEventHandler.send(PortfolioEvent.OrganizeTokens)
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onTokenItemClick(currency: CryptoCurrency) {
// TODO
analyticsEventHandler.send(PortfolioEvent.TokenTapped)
router.openTokenDetails(stateHolder.getSelectedWalletId(), currency)
}
override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
// TODO
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) {
// TODO
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

@ -1,6 +1,46 @@
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 kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import javax.inject.Inject
interface WalletCurrencyActionsClickIntents {
@ -24,41 +64,353 @@ interface WalletCurrencyActionsClickIntents {
fun onExploreClick()
}
internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor() : WalletCurrencyActionsClickIntents {
@Suppress("LongParameterList", "LargeClass")
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) {
// TODO
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) {
// TODO
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) {
// TODO
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) {
// TODO
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) {
// TODO
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) {
// TODO
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) {
// TODO
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) {
// TODO
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol),
)
reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency))
}
override fun onExploreClick() {
// TODO
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

@ -1,6 +1,44 @@
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 kotlinx.coroutines.launch
import javax.inject.Inject
internal interface WalletWarningsClickIntents {
@ -24,41 +62,224 @@ internal interface WalletWarningsClickIntents {
fun onCloseRateAppWarningClick()
}
internal class WalletWarningsClickIntentsImplementer @Inject constructor() : WalletWarningsClickIntents {
@Suppress("LongParameterList")
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() {
// TODO
analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped)
router.openOnboardingScreen()
}
override fun onCloseAlreadySignedHashesWarningClick() {
// TODO
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
viewModelScope.launch(dispatchers.main) {
setCardWasScannedUseCase(cardId = userWallet.cardId)
}
}
override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>) {
// TODO
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() {
// TODO
stateHolder.update(
OpenBottomSheetTransformer(
content = requireNotNull(stateHolder.getSelectedWallet().bottomSheetConfig).content,
userWalletId = stateHolder.getSelectedWalletId(),
onDismissBottomSheet = {
stateHolder.update(
CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()),
)
},
),
)
}
override fun onUnlockWalletClick() {
// TODO
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() {
// TODO
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() {
// TODO
analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked))
walletEventSender.send(
event = WalletEvent.RateApp(
onDismissClick = {
viewModelScope.launch(dispatchers.main) {
neverToSuggestRateAppUseCase()
}
},
),
)
}
override fun onDislikeAppClick() {
// TODO
analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked))
viewModelScope.launch(dispatchers.main) {
neverToSuggestRateAppUseCase()
reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter)
}
}
override fun onCloseRateAppWarningClick() {
// TODO
analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed))
viewModelScope.launch(dispatchers.main) {
remindToRateAppLaterUseCase()
}
}
}