diff --git a/app/src/main/java/to/bitkit/repositories/SuggestionsRepo.kt b/app/src/main/java/to/bitkit/repositories/SuggestionsRepo.kt new file mode 100644 index 0000000000..aa3317e571 --- /dev/null +++ b/app/src/main/java/to/bitkit/repositories/SuggestionsRepo.kt @@ -0,0 +1,99 @@ +package to.bitkit.repositories + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import to.bitkit.data.SettingsData +import to.bitkit.data.SettingsStore +import to.bitkit.data.entities.TransferEntity +import to.bitkit.di.BgDispatcher +import to.bitkit.models.Suggestion +import to.bitkit.models.TransferType +import to.bitkit.models.toSuggestionOrNull +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SuggestionsRepo @Inject constructor( + @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val walletRepo: WalletRepo, + private val settingsStore: SettingsStore, + private val transferRepo: TransferRepo, + private val pubkyRepo: PubkyRepo, +) { + companion object { + private const val MAX_SUGGESTIONS = 4 + } + + val suggestionsFlow: Flow> = combine( + walletRepo.balanceState, + settingsStore.data, + transferRepo.activeTransfers, + pubkyRepo.isAuthenticated, + ) { balanceState, settings, transfers, profileAuthenticated -> + val baseSuggestions = when { + balanceState.totalLightningSats > 0uL -> + spendingSuggestions(settings, profileAuthenticated) + balanceState.totalOnchainSats > 0uL -> + savingsOnlySuggestions(settings, transfers, profileAuthenticated) + else -> emptyWalletSuggestions(settings, transfers, profileAuthenticated) + } + val dismissedList = settings.dismissedSuggestions.mapNotNull { it.toSuggestionOrNull() } + baseSuggestions + .filterNot { it in dismissedList } + .take(MAX_SUGGESTIONS) + } + + suspend fun resetDismissedSuggestionsIfEmpty() = withContext(bgDispatcher) { + if (suggestionsFlow.first().isEmpty()) { + settingsStore.update { it.copy(dismissedSuggestions = emptyList()) } + } + } + + private fun spendingSuggestions( + settings: SettingsData, + profileAuthenticated: Boolean, + ) = listOfNotNull( + Suggestion.QUICK_PAY.takeIf { !settings.isQuickPayEnabled }, + Suggestion.NOTIFICATIONS.takeIf { !settings.notificationsGranted }, + Suggestion.SHOP, + Suggestion.PROFILE.takeIf { !profileAuthenticated }, + Suggestion.SUPPORT, + Suggestion.INVITE, + Suggestion.BUY, + ) + + private fun savingsOnlySuggestions( + settings: SettingsData, + transfers: List, + profileAuthenticated: Boolean, + ) = listOfNotNull( + Suggestion.BACK_UP.takeIf { !settings.backupVerified }, + Suggestion.SECURE.takeIf { !settings.isPinEnabled }, + Suggestion.LIGHTNING.takeIf { + transfers.all { it.type != TransferType.TO_SPENDING } + }, + Suggestion.SUPPORT, + Suggestion.PROFILE.takeIf { !profileAuthenticated }, + Suggestion.INVITE, + Suggestion.BUY, + ) + + private fun emptyWalletSuggestions( + settings: SettingsData, + transfers: List, + profileAuthenticated: Boolean, + ) = listOfNotNull( + Suggestion.BUY, + Suggestion.LIGHTNING.takeIf { + transfers.all { it.type != TransferType.TO_SPENDING } + }, + Suggestion.SUPPORT, + Suggestion.BACK_UP.takeIf { !settings.backupVerified }, + Suggestion.SECURE.takeIf { !settings.isPinEnabled }, + Suggestion.PROFILE.takeIf { !profileAuthenticated }, + Suggestion.INVITE, + ) +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 1af5a07764..47996abb6b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -168,6 +168,7 @@ import to.bitkit.ui.screens.widgets.headlines.HeadlineCard import to.bitkit.ui.screens.widgets.headlines.HeadlineCardSmall import to.bitkit.ui.screens.widgets.price.PriceCard import to.bitkit.ui.screens.widgets.price.PriceCardSmall +import to.bitkit.ui.screens.widgets.suggestions.SuggestionsPreviewGrid import to.bitkit.ui.screens.widgets.weather.WeatherCard import to.bitkit.ui.screens.widgets.weather.WeatherCardSmall import to.bitkit.ui.shared.modifiers.clickableAlpha @@ -1174,15 +1175,20 @@ private fun WidgetCardContent( } WidgetType.SUGGESTIONS -> { - if (homeUiState.suggestions.isEmpty()) { - WidgetEditPlaceholder(small = false) - } else { - SuggestionsSection( + when { + homeUiState.suggestions.isNotEmpty() -> SuggestionsSection( suggestions = homeUiState.suggestions, onRemoveSuggestion = onRemoveSuggestion, onClickSuggestion = onClickSuggestion, modifier = Modifier.fillMaxWidth() ) + + homeUiState.isEditingWidgets -> SuggestionsPreviewGrid( + onSuggestionClick = {}, + modifier = Modifier.fillMaxWidth() + ) + + else -> WidgetEditPlaceholder(small = false) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt index b06516e4be..85b35b53ef 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt @@ -18,21 +18,18 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.data.SettingsData import to.bitkit.data.SettingsStore -import to.bitkit.data.entities.TransferEntity import to.bitkit.models.ActivityBannerType import to.bitkit.models.BannerItem import to.bitkit.models.Suggestion -import to.bitkit.models.TransferType import to.bitkit.models.WidgetType -import to.bitkit.models.toSuggestionOrNull import to.bitkit.models.widget.ArticleModel import to.bitkit.models.widget.toArticleModel import to.bitkit.models.widget.toBlockModel import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.PubkyRepo +import to.bitkit.repositories.SuggestionsRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo import to.bitkit.repositories.WidgetsRepo @@ -51,12 +48,9 @@ class HomeViewModel @Inject constructor( private val transferRepo: TransferRepo, private val pubkyRepo: PubkyRepo, private val activityRepo: ActivityRepo, + private val suggestionsRepo: SuggestionsRepo, ) : ViewModel() { - companion object { - private const val MAX_SUGGESTIONS = 4 - } - private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -115,7 +109,7 @@ class HomeViewModel @Inject constructor( } viewModelScope.launch { - createSuggestionsFlow().collect { suggestions -> + suggestionsRepo.suggestionsFlow.collect { suggestions -> _uiState.update { it.copy(suggestions = suggestions.toImmutableList()) } } } @@ -306,68 +300,4 @@ class HomeViewModel @Inject constructor( _uiState.update { it.copy(banners = banners.toImmutableList()) } } } - - private fun createSuggestionsFlow() = combine( - walletRepo.balanceState, - settingsStore.data, - transferRepo.activeTransfers, - pubkyRepo.isAuthenticated, - ) { balanceState, settings, transfers, profileAuthenticated -> - val baseSuggestions = when { - balanceState.totalLightningSats > 0uL -> - spendingSuggestions(settings, profileAuthenticated) - balanceState.totalOnchainSats > 0uL -> - savingsOnlySuggestions(settings, transfers, profileAuthenticated) - else -> emptyWalletSuggestions(settings, transfers, profileAuthenticated) - } - val dismissedList = settings.dismissedSuggestions.mapNotNull { it.toSuggestionOrNull() } - baseSuggestions - .filterNot { it in dismissedList } - .take(MAX_SUGGESTIONS) - } - - private fun spendingSuggestions( - settings: SettingsData, - profileAuthenticated: Boolean, - ) = listOfNotNull( - Suggestion.QUICK_PAY.takeIf { !settings.isQuickPayEnabled }, - Suggestion.NOTIFICATIONS.takeIf { !settings.notificationsGranted }, - Suggestion.SHOP, - Suggestion.PROFILE.takeIf { !profileAuthenticated }, - Suggestion.SUPPORT, - Suggestion.INVITE, - Suggestion.BUY, - ) - - private fun savingsOnlySuggestions( - settings: SettingsData, - transfers: List, - profileAuthenticated: Boolean, - ) = listOfNotNull( - Suggestion.BACK_UP.takeIf { !settings.backupVerified }, - Suggestion.SECURE.takeIf { !settings.isPinEnabled }, - Suggestion.LIGHTNING.takeIf { - transfers.all { it.type != TransferType.TO_SPENDING } - }, - Suggestion.SUPPORT, - Suggestion.PROFILE.takeIf { !profileAuthenticated }, - Suggestion.INVITE, - Suggestion.BUY, - ) - - private fun emptyWalletSuggestions( - settings: SettingsData, - transfers: List, - profileAuthenticated: Boolean, - ) = listOfNotNull( - Suggestion.BUY, - Suggestion.LIGHTNING.takeIf { - transfers.all { it.type != TransferType.TO_SPENDING } - }, - Suggestion.SUPPORT, - Suggestion.BACK_UP.takeIf { !settings.backupVerified }, - Suggestion.SECURE.takeIf { !settings.isPinEnabled }, - Suggestion.PROFILE.takeIf { !profileAuthenticated }, - Suggestion.INVITE, - ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt index 4bc29cc33d..b11ad0522c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt @@ -9,12 +9,14 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import to.bitkit.models.WidgetType +import to.bitkit.repositories.SuggestionsRepo import to.bitkit.repositories.WidgetsRepo import javax.inject.Inject @HiltViewModel class SuggestionsViewModel @Inject constructor( private val widgetsRepo: WidgetsRepo, + private val suggestionsRepo: SuggestionsRepo, ) : ViewModel() { companion object { @@ -29,6 +31,7 @@ class SuggestionsViewModel @Inject constructor( fun addWidget(onComplete: () -> Unit = {}) { viewModelScope.launch { + suggestionsRepo.resetDismissedSuggestionsIfEmpty() widgetsRepo.addWidget(WidgetType.SUGGESTIONS) onComplete() } diff --git a/app/src/test/java/to/bitkit/repositories/SuggestionsRepoTest.kt b/app/src/test/java/to/bitkit/repositories/SuggestionsRepoTest.kt new file mode 100644 index 0000000000..8ff5ea159c --- /dev/null +++ b/app/src/test/java/to/bitkit/repositories/SuggestionsRepoTest.kt @@ -0,0 +1,78 @@ +package to.bitkit.repositories + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import to.bitkit.data.SettingsData +import to.bitkit.data.SettingsStore +import to.bitkit.models.BalanceState +import to.bitkit.models.Suggestion +import to.bitkit.test.BaseUnitTest +import kotlin.test.assertTrue + +class SuggestionsRepoTest : BaseUnitTest() { + private lateinit var sut: SuggestionsRepo + + private val walletRepo = mock() + private val settingsStore = mock() + private val transferRepo = mock() + private val pubkyRepo = mock() + + private fun setUp(settings: SettingsData) { + whenever(walletRepo.balanceState).thenReturn(MutableStateFlow(BalanceState())) + whenever(settingsStore.data).thenReturn(flowOf(settings)) + whenever(transferRepo.activeTransfers).thenReturn(flowOf(emptyList())) + whenever(pubkyRepo.isAuthenticated).thenReturn(MutableStateFlow(false)) + + sut = SuggestionsRepo( + bgDispatcher = testDispatcher, + walletRepo = walletRepo, + settingsStore = settingsStore, + transferRepo = transferRepo, + pubkyRepo = pubkyRepo, + ) + } + + @Before + fun before() { + setUp(SettingsData()) + } + + @Test + fun `resetDismissedSuggestionsIfEmpty clears dismissed when no suggestions are visible`() = test { + setUp(SettingsData(dismissedSuggestions = Suggestion.entries.map { it.name })) + + sut.resetDismissedSuggestionsIfEmpty() + + val captor = argumentCaptor<(SettingsData) -> SettingsData>() + verify(settingsStore).update(captor.capture()) + val result = captor.firstValue(SettingsData(dismissedSuggestions = listOf(Suggestion.BUY.name))) + assertTrue(result.dismissedSuggestions.isEmpty()) + } + + @Test + fun `resetDismissedSuggestionsIfEmpty does nothing when suggestions are visible`() = test { + setUp(SettingsData(dismissedSuggestions = emptyList())) + + sut.resetDismissedSuggestionsIfEmpty() + + verify(settingsStore, never()).update(any()) + } + + @Test + fun `suggestionsFlow filters out dismissed suggestions`() = test { + setUp(SettingsData(dismissedSuggestions = listOf(Suggestion.BUY.name))) + + val suggestions = sut.suggestionsFlow.first() + + assertTrue(Suggestion.BUY !in suggestions) + } +} diff --git a/changelog.d/next/1016.fixed.md b/changelog.d/next/1016.fixed.md new file mode 100644 index 0000000000..c2486a2adb --- /dev/null +++ b/changelog.d/next/1016.fixed.md @@ -0,0 +1 @@ +Re-adding the Suggestions widget now restores the default suggestion cards when all of them had been dismissed.