diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index f2b2d1f70f..8e206bdeb7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -8,7 +8,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -22,15 +21,18 @@ class GetCurrencyWarningsUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { - suspend operator fun invoke(userWallet: UserWallet, currency: CryptoCurrency): Flow> { + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow> { return combine( getFeeWarningFlow( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, networkId = currency.network.id, currencyId = currency.id, ), - flowOf(walletManagersFacade.getRentInfo(userWallet.walletId, currency.network)), - flowOf(walletManagersFacade.getExistentialDeposit(userWallet.walletId, currency.network)), + flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), + flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), ) { maybeFeeWarning, maybeRentWarning, maybeEdWarning -> setOfNotNull( maybeRentWarning, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 2ff8ce19a2..e3b2eca4d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -83,6 +83,7 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, + notifications = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions {}, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 9c9ff33f4b..cdf8590f46 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -5,7 +5,9 @@ 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.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList internal data class TokenDetailsState( @@ -13,6 +15,7 @@ internal data class TokenDetailsState( val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val notifications: ImmutableList, val pendingTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt new file mode 100644 index 0000000000..ea35a155a6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning +import com.tangem.features.tokendetails.impl.R + +// TODO: Finalize notification strings [REDACTED_JIRA] +@Immutable +sealed class TokenDetailsNotification(open val config: NotificationConfig) { + + data class RentInfoNotification( + private val rentInfo: CryptoCurrencyWarning.Rent, + private val onCloseClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Res(R.string.send_network_fee_title), + subtitle = TextReference.Res( + id = R.string.solana_rent_warning, + formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount), + ), + iconResId = R.drawable.img_attention_20, + onCloseClick = onCloseClick, + ), + ) + + data class ExistentialDepositNotification( + private val existentialInfo: CryptoCurrencyWarning.ExistentialDeposit, + private val onCloseClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Str("Existential Deposit"), + subtitle = TextReference.Res( + id = R.string.warning_existential_deposit_message, + formatArgs = wrappedList(existentialInfo.currencyName, existentialInfo.edStringValueWithSymbol), + ), + iconResId = R.drawable.img_attention_20, + onCloseClick = onCloseClick, + ), + ) + + data class NetworkFeeFeeNotification( + private val feeInfo: CryptoCurrencyWarning.BalanceNotEnoughForFee, + private val onBuyClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Res( + id = R.string.notification_title_not_enough_funds, + formatArgs = wrappedList(feeInfo.blockchainFullName), + ), + subtitle = TextReference.Res( + id = R.string.token_details_send_blocked_fee_format, + formatArgs = wrappedList( + feeInfo.currency.name, + feeInfo.blockchainFullName, + feeInfo.currency.name, + feeInfo.blockchainFullName, + feeInfo.blockchainSymbol, + ), + ), + iconResId = feeInfo.currency.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = TextReference.Str("Buy"), + onClick = onBuyClick, + ), + ), + ) + + object NetworksUnreachableNotification : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Str("Some networks are unreachable"), + subtitle = TextReference.Str("The problem is on the crypto-network side. It will be fixed soon."), + iconResId = R.drawable.img_attention_20, + ), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 95fa7c04c5..d881dbe36a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -10,8 +10,10 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryToTransactionStateConverter import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -32,8 +34,12 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun convertError(): TokenDetailsState { - // TODO: [REDACTED_JIRA] - return currentStateProvider() + val state = currentStateProvider() + return state.copy( + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons), + marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencyName), + notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachableNotification), + ) } private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { @@ -68,7 +74,6 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.NoAmount, - // TODO: [REDACTED_JIRA] is CryptoCurrencyStatus.Unreachable, -> { TokenDetailsBalanceBlockState.Error(currentState.actionButtons) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt new file mode 100644 index 0000000000..9656012880 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.removeBy +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class TokenDetailsNotificationConverter( + private val clickIntents: TokenDetailsClickIntents, +) : Converter, ImmutableList> { + + override fun convert(value: Set): ImmutableList { + return value.map(::mapToNotification).toImmutableList() + } + + fun removeExistentialDeposit(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is TokenDetailsNotification.ExistentialDepositNotification } + return newNotifications.toImmutableList() + } + + fun removeRentInfo(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is TokenDetailsNotification.RentInfoNotification } + return newNotifications.toImmutableList() + } + + private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { + return when (warning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFeeFeeNotification( + feeInfo = warning, + onBuyClick = clickIntents::onBuyClick, + ) + is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDepositNotification( + existentialInfo = warning, + onCloseClick = clickIntents::onCloseExistentialDepositNotification, + ) + is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.RentInfoNotification( + rentInfo = warning, + onCloseClick = clickIntents::onCloseRentInfoNotification, + ) + CryptoCurrencyWarning.SomeNetworksUnreachable -> TokenDetailsNotification.NetworksUnreachableNotification + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index ce9e0bba6e..3d5bc47a22 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -43,6 +43,7 @@ internal class TokenDetailsSkeletonStateConverter( actionButtons = createButtons(), ), marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name), + notifications = persistentListOf(), pendingTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index c381e21449..874f8bc7e9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -12,6 +12,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError @@ -36,6 +37,10 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter(clickIntents = clickIntents) } + private val notificationConverter by lazy { + TokenDetailsNotificationConverter(clickIntents = clickIntents) + } + private val tokenDetailsLoadedBalanceConverter by lazy { TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, @@ -177,4 +182,19 @@ internal class TokenDetailsStateFactory( ) } ?: return currentState } + + fun getStateWithNotifications(warnings: Set): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.convert(warnings)) + } + + fun getStateWithRemovedExistentialNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeExistentialDeposit(state)) + } + + fun getStateWithRemovedRentNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeRentInfo(state)) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index b3d71d3409..4241d5707c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,8 +1,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh @@ -18,6 +20,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.Transaction import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -31,7 +34,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock import kotlinx.collections.immutable.PersistentList -@OptIn(ExperimentalMaterialApi::class) +@Suppress("LongMethod") +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { Scaffold( @@ -72,6 +76,12 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + items( + items = state.notifications, + key = { it.config.title.hashCode() }, + contentType = { it.config::class.java }, + itemContent = { Notification(config = it.config, modifier = itemModifier.animateItemPlacement()) }, + ) item( key = MarketPriceBlockState::class.java, contentType = MarketPriceBlockState::class.java, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 40bef9b4c4..9881457874 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -27,4 +27,8 @@ interface TokenDetailsClickIntents { fun onExploreClick() fun onDismissBottomSheet() + + fun onCloseRentInfoNotification() + + fun onCloseExistentialDepositNotification() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index e08a08f68c..9ef4aaafa0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -53,6 +53,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, savedStateHandle: SavedStateHandle, @@ -100,6 +101,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet) { updateMarketPrice(selectedWallet = selectedWallet) updateTxHistory() + updateWarnings(selectedWallet = selectedWallet) } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -126,6 +128,18 @@ internal class TokenDetailsViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun updateWarnings(selectedWallet: UserWallet) { + viewModelScope.launch { + getCurrencyWarningsUseCase.invoke( + userWalletId = selectedWallet.walletId, + currency = cryptoCurrency, + ) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getStateWithNotifications(it) } + .launchIn(viewModelScope) + } + } + private fun updateMarketPrice(selectedWallet: UserWallet) { getCurrencyStatusUpdatesUseCase( userWalletId = selectedWallet.walletId, @@ -315,4 +329,12 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onDismissBottomSheet() { uiState = stateFactory.getStateWithClosedBottomSheet() } + + override fun onCloseExistentialDepositNotification() { + uiState = stateFactory.getStateWithRemovedExistentialNotification() + } + + override fun onCloseRentInfoNotification() { + uiState = stateFactory.getStateWithRemovedRentNotification() + } } \ No newline at end of file