diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt new file mode 100644 index 0000000000..7e62eb79e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import kotlinx.coroutines.flow.* +import timber.log.Timber + +internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { + return this().fold( + ifLeft = { + Timber.e("Impossible to get selected wallet $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return this(userWalletId) + .conflate() + .distinctUntilChanged() + .filter(Either::isRight) + .firstOrNull() + ?.fold( + ifLeft = { + Timber.e("Impossible to get primary currency status $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetSelectedAppCurrencyUseCase.unwrap(): AppCurrency { + return this() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .firstOrNull() + ?: AppCurrency.Default +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.collectLatest( + userWalletId: UserWalletId, + onRight: suspend (CryptoCurrencyStatus) -> Unit, +) { + this(userWalletId = userWalletId) + .conflate() + .distinctUntilChanged() + .collectLatest { maybeStatus -> + maybeStatus.onRight { onRight(it) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt new file mode 100644 index 0000000000..d9be6984f5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import kotlinx.coroutines.CoroutineScope +import kotlin.properties.Delegates + +/** + * Base wallet click intents component. + * Provides router and viewModelScope to child classes. + * +[REDACTED_AUTHOR] + */ +@Suppress("UnnecessaryAbstractClass") +internal abstract class BaseWalletClickIntents { + + protected val router: InnerWalletRouter get() = _router + protected val viewModelScope: CoroutineScope get() = _viewModelScope + + private var _router: InnerWalletRouter by Delegates.notNull() + private var _viewModelScope: CoroutineScope by Delegates.notNull() + + open fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + _router = router + _viewModelScope = coroutineScope + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 29df476122..e7cc3d17a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -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, + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt index 4903e65c4d..643c5dec03 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -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), + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 02fc91d677..82c943e41b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -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), + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index dd10ad4337..0b6070971a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -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
, + ): 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
, + currency: CryptoCurrency, + ) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = ChooseAddressBottomSheetConfig( + addressModels = addresses + .map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + } + .toImmutableList(), + onClick = { + onAddressTypeSelected( + userWalletId = userWalletId, + currency = currency, + addressModel = it, + ) + }, + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + }, + ), + ) + } + + private fun onAddressTypeSelected( + userWalletId: UserWalletId, + currency: CryptoCurrency, + addressModel: AddressModel, + ) { + viewModelScope.launch(dispatchers.main) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.valueOf(addressModel.type.name), + ), + ) + + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + } + } + + private fun showErrorIfDemoModeOrElse(action: () -> Unit) { + val cardId = getSelectedWalletSyncUseCase.unwrap()?.cardId ?: return + + if (isDemoCardUseCase(cardId = cardId)) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + + walletEventSender.send( + event = WalletEvent.ShowError( + text = resourceReference(id = R.string.alert_demo_feature_disabled), + ), + ) + } else { + action() + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 99dfe53ffb..b4b9b2d06f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -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) { - // 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, + onSuccess: suspend (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value))?.let { curve -> + getNewDerivations(curve, scanResponse, it) + } + } + + val derivations = buildMap> { + derivationDataList.forEach { + val current = this[it.derivations.first] + if (current != null) { + current.addAll(it.derivations.second) + current.distinct() + } else { + this[it.derivations.first] = it.derivations.second.toMutableList() + } + } + }.ifEmpty { return } + + viewModelScope.launch(dispatchers.io) { + derivePublicKeysUseCase(cardId = null, derivations = derivations) + .onRight { + val newDerivedKeys = it.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) + + onSuccess(updatedScanResponse) + } + } + } + + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currency: CryptoCurrency, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val blockchain = Blockchain.fromId(currency.network.id.value) + val supportedCurves = blockchain.getSupportedCurves() + val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) + .takeIf { supportedCurves.contains(curve) } + + val customPath = currency.network.derivationPath.value?.let { + DerivationPath(it) + }.takeIf { supportedCurves.contains(curve) } + + val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) { + currency.network.derivationPath.value?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + + class DerivationData(val derivations: Pair>) + override fun onOpenUnlockWalletsBottomSheetClick() { - // 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() + } } } \ No newline at end of file