Skip to content

Commit

Permalink
Handle user refreshes
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Oct 30, 2024
1 parent 6284500 commit d248f95
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import com.duckduckgo.app.browser.camera.CameraHardwareChecker
import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository
import com.duckduckgo.app.browser.certificates.remoteconfig.SSLCertificatesFeature
import com.duckduckgo.app.browser.commands.Command
import com.duckduckgo.app.browser.commands.Command.HideBrokenSitePromptCta
import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxDialog
import com.duckduckgo.app.browser.commands.Command.LaunchPrivacyPro
import com.duckduckgo.app.browser.commands.Command.LoadExtractedUrl
Expand Down Expand Up @@ -105,6 +106,7 @@ import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter
import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccessFavorite
import com.duckduckgo.app.browser.omnibar.ChangeOmnibarPositionFeature
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.browser.omnibar.QueryOrigin.FromUser
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.BOTTOM
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.TOP
import com.duckduckgo.app.browser.refreshpixels.RefreshPixelSender
Expand All @@ -124,6 +126,7 @@ import com.duckduckgo.app.cta.model.CtaId.DAX_DIALOG_NETWORK
import com.duckduckgo.app.cta.model.CtaId.DAX_DIALOG_TRACKERS_FOUND
import com.duckduckgo.app.cta.model.CtaId.DAX_END
import com.duckduckgo.app.cta.model.DismissedCta
import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta
import com.duckduckgo.app.cta.ui.Cta
import com.duckduckgo.app.cta.ui.CtaViewModel
import com.duckduckgo.app.cta.ui.DaxBubbleCta
Expand Down Expand Up @@ -542,7 +545,6 @@ class BrowserTabViewModelTest {
whenever(changeOmnibarPositionFeature.refactor()).thenReturn(mockEnabledToggle)
whenever(mockAutocompleteTabsFeature.self()).thenReturn(mockEnabledToggle)
whenever(mockAutocompleteTabsFeature.self().isEnabled()).thenReturn(true)
whenever(mockBrokenSitePrompt.isFeatureEnabled()).thenReturn(false)

remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider)

Expand Down Expand Up @@ -666,6 +668,7 @@ class BrowserTabViewModelTest {
refreshPixelSender = refreshPixelSender,
changeOmnibarPositionFeature = changeOmnibarPositionFeature,
highlightsOnboardingExperimentManager = mockHighlightsOnboardingExperimentManager,
brokenSitePrompt = mockBrokenSitePrompt,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -738,6 +741,13 @@ class BrowserTabViewModelTest {
assertTrue(commandCaptor.allValues.contains(Command.HideKeyboard))
}

@Test
fun whenViewBecomesVisibleThenc() {
setBrowserShowing(true)
testee.onViewVisible()
verify(mockBrokenSitePrompt).resetRefreshCount()
}

@Test
fun whenViewBecomesVisibleAndHomeShowingThenRefreshCtaIsCalled() {
runTest {
Expand Down Expand Up @@ -775,6 +785,16 @@ class BrowserTabViewModelTest {
assertCommandIssued<Command.ShowErrorWithAction>()
}

@Test
fun whenSubmittedQueryThenResetRefreshCount() {
whenever(mockSpecialUrlDetector.determineType(anyString())).thenReturn(SpecialUrlDetector.UrlType.Web("https://example.com"))
whenever(mockOmnibarConverter.convertQueryToUrl(any(), eq(null), eq(FromUser))).thenReturn("https://example.com")

testee.onUserSubmittedQuery("https://example.com")

verify(mockBrokenSitePrompt).resetRefreshCount()
}

@Test
fun whenSubmittedQueryIsPrivacyProThenSendLaunchPrivacyProComment() {
whenever(mockSpecialUrlDetector.determineType(anyString())).thenReturn(SpecialUrlDetector.UrlType.ShouldLaunchPrivacyProLink)
Expand Down Expand Up @@ -2428,13 +2448,27 @@ class BrowserTabViewModelTest {
verify(mockPixel).fire(AppPixelName.TAB_MANAGER_NEW_TAB_LONG_PRESSED)
}

@Test
fun whenCloseCurrentTabThenResetRefreshCount() = runTest {
givenOneActiveTabSelected()
testee.closeCurrentTab()
verify(mockBrokenSitePrompt).resetRefreshCount()
}

@Test
fun whenCloseCurrentTabSelectedThenTabDeletedFromRepository() = runTest {
givenOneActiveTabSelected()
testee.closeCurrentTab()
verify(mockTabRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId)
}

@Test
fun whenCloseAndSelectSourceTabSelectedThenTabDeletedFromRepository() = runTest {
givenOneActiveTabSelected()
testee.closeAndSelectSourceTab()
verify(mockTabRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId)
}

@Test
fun whenUserPressesBackAndSkippingHomeThenWebViewPreviewGenerated() {
setupNavigation(isBrowsing = true, canGoBack = false, skipHome = true)
Expand Down Expand Up @@ -5311,6 +5345,14 @@ class BrowserTabViewModelTest {
}
}

@Test
fun whenRefreshIsTriggeredByUserThenIncrementRefreshCount() = runTest {
testee.onRefreshRequested(triggeredByUser = false)
verify(mockBrokenSitePrompt, never()).incrementRefreshCount()
testee.onRefreshRequested(triggeredByUser = true)
verify(mockBrokenSitePrompt).incrementRefreshCount()
}

@Test
fun whenRefreshIsTriggeredByUserThenPrivacyProtectionsPopupManagerIsNotifiedWithTopPosition() = runTest {
testee.onRefreshRequested(triggeredByUser = false)
Expand Down Expand Up @@ -5936,6 +5978,24 @@ class BrowserTabViewModelTest {
assertTrue(browserViewState().showDuckPlayerIcon)
}

@Test
fun whenNewPageAndBrokenSitePromptVisibleThenHideCta() = runTest {
setCta(BrokenSitePromptDialogCta())

testee.browserViewState.value = browserViewState().copy(browserShowing = true)
testee.navigationStateChanged(buildWebNavigation("https://example.com"))

assertCommandIssued<HideBrokenSitePromptCta>()
}

@Test
fun whenNewPageThenResetRefreshCount() = runTest {
testee.browserViewState.value = browserViewState().copy(browserShowing = true)
testee.navigationStateChanged(buildWebNavigation("https://example.com"))

verify(mockBrokenSitePrompt).resetRefreshCount()
}

@Test
fun whenUrlUpdatedWithUrlYouTubeNoCookieThenReplaceUrlWithDuckPlayer() = runTest {
whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class CtaViewModelTest {
whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(false, AlwaysAsk))
whenever(mockDuckPlayer.isYouTubeUrl(any())).thenReturn(false)
whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false)
whenever(mockBrokenSitePrompt.isFeatureEnabled()).thenReturn(false)
whenever(mockBrokenSitePrompt.shouldShowBrokenSitePrompt()).thenReturn(false)

testee = CtaViewModel(
appInstallStore = mockAppInstallStore,
Expand Down Expand Up @@ -282,9 +282,9 @@ class CtaViewModelTest {
}

@Test
fun whenRefreshCtaWhileBrowsingAndHideTipsIsTrueAndBrokenSitePromptEnabledThenReturnBrokenSitePrompt() = runTest {
fun whenRefreshCtaWhileBrowsingAndHideTipsIsTrueAndShouldShowBrokenSitePromptThenReturnBrokenSitePrompt() = runTest {
whenever(mockSettingsDataStore.hideTips).thenReturn(true)
whenever(mockBrokenSitePrompt.isFeatureEnabled()).thenReturn(true)
whenever(mockBrokenSitePrompt.shouldShowBrokenSitePrompt()).thenReturn(true)
val site = site(url = "http://www.facebook.com", entity = TestEntity("Facebook", "Facebook", 9.0))

val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor
import com.duckduckgo.brokensite.api.BrokenSitePrompt
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.browser.api.brokensite.BrokenSiteData
import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.MENU
Expand Down Expand Up @@ -430,6 +431,7 @@ class BrowserTabViewModel @Inject constructor(
private val refreshPixelSender: RefreshPixelSender,
private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature,
private val highlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager,
private val brokenSitePrompt: BrokenSitePrompt,
) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -829,6 +831,7 @@ class BrowserTabViewModel @Inject constructor(
viewModelScope.launch {
refreshOnViewVisible.emit(true)
}
brokenSitePrompt.resetRefreshCount()
}

fun onViewHidden() {
Expand Down Expand Up @@ -962,6 +965,7 @@ class BrowserTabViewModel @Inject constructor(
query: String,
queryOrigin: QueryOrigin = QueryOrigin.FromUser,
) {
brokenSitePrompt.resetRefreshCount()
navigationAwareLoginDetector.onEvent(NavigationEvent.UserAction.NewQuerySubmitted)

if (query.isBlank()) {
Expand Down Expand Up @@ -1172,7 +1176,10 @@ class BrowserTabViewModel @Inject constructor(
override fun isDesktopSiteEnabled(): Boolean = currentBrowserViewState().isDesktopBrowsingMode

override fun closeCurrentTab() {
viewModelScope.launch { removeCurrentTabFromRepository() }
viewModelScope.launch {
removeCurrentTabFromRepository()
brokenSitePrompt.resetRefreshCount()
}
}

fun closeAndReturnToSourceIfBlankTab() {
Expand All @@ -1182,7 +1189,10 @@ class BrowserTabViewModel @Inject constructor(
}

override fun closeAndSelectSourceTab() {
viewModelScope.launch { removeAndSelectTabFromRepository() }
viewModelScope.launch {
removeAndSelectTabFromRepository()
brokenSitePrompt.resetRefreshCount()
}
}

private suspend fun removeAndSelectTabFromRepository() {
Expand Down Expand Up @@ -1213,6 +1223,7 @@ class BrowserTabViewModel @Inject constructor(

if (triggeredByUser) {
site?.realBrokenSiteContext?.onUserTriggeredRefresh()
brokenSitePrompt.incrementRefreshCount()
privacyProtectionsPopupManager.onPageRefreshTriggeredByUser(isOmnibarAtTheTop = settingsDataStore.omnibarPosition == TOP)
}
}
Expand Down Expand Up @@ -1357,6 +1368,14 @@ class BrowserTabViewModel @Inject constructor(
pageChanged(stateChange.url, stateChange.title)
}
}
ctaViewState.value?.cta?.let { cta ->
if (cta is BrokenSitePromptDialogCta) {
withContext(dispatchers.main()) {
command.value = HideBrokenSitePromptCta(cta)
}
}
}
brokenSitePrompt.resetRefreshCount()
}
}

Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,7 @@ class CtaViewModel @Inject constructor(
}

if (!canShowOnboardingDaxDialogCta()) {
return if (brokenSitePrompt.isFeatureEnabled()) {
// TODO (cbarreiro) Add logic to decide whether or not to show the prompt
return if (brokenSitePrompt.shouldShowBrokenSitePrompt()) {
BrokenSitePromptDialogCta()
} else {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@ interface BrokenSitePrompt {
suspend fun userAcceptedPrompt()

fun isFeatureEnabled(): Boolean

fun incrementRefreshCount()

fun resetRefreshCount()

fun getUserRefreshesCount(): Int

fun shouldShowBrokenSitePrompt(): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.brokensite.impl

import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import java.time.LocalDateTime
import javax.inject.Inject

interface BrokenSitePromptInMemoryStore {
fun resetRefreshCount()
fun addRefresh(localDateTime: LocalDateTime)
fun getAndUpdateUserRefreshesBetween(
t1: LocalDateTime,
t2: LocalDateTime,
): Int
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class RealBrokenSitePromptInMemoryStore @Inject constructor() : BrokenSitePromptInMemoryStore {
private var refreshes = mutableListOf<LocalDateTime>()

override fun resetRefreshCount() {
this.refreshes = mutableListOf()
}

override fun addRefresh(localDateTime: LocalDateTime) {
refreshes.add(localDateTime)
}

override fun getAndUpdateUserRefreshesBetween(
t1: LocalDateTime,
t2: LocalDateTime,
): Int {
refreshes = refreshes.filter { it.isAfter(t1) && it.isBefore(t2) }.toMutableList()
return refreshes.size
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ interface BrokenSiteReportRepository {
suspend fun incrementDismissStreak()
fun getDismissStreak(): Int
suspend fun resetDismissStreak()
fun resetRefreshCount()
fun addRefresh(localDateTime: LocalDateTime)
fun getAndUpdateUserRefreshesBetween(t1: LocalDateTime, t2: LocalDateTime): Int
}

class RealBrokenSiteReportRepository(
private val database: BrokenSiteDatabase,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
private val brokenSitePromptDataStore: BrokenSitePomptDataStore,
private val brokenSitePromptInMemoryStore: BrokenSitePromptInMemoryStore,
) : BrokenSiteReportRepository {

override suspend fun getLastSentDay(hostname: String): String? {
Expand Down Expand Up @@ -146,6 +150,18 @@ class RealBrokenSiteReportRepository(
brokenSitePromptDataStore.setDismissStreak(dismissCount + 1)
}

override fun resetRefreshCount() {
brokenSitePromptInMemoryStore.resetRefreshCount()
}

override fun addRefresh(localDateTime: LocalDateTime) {
brokenSitePromptInMemoryStore.addRefresh(localDateTime)
}

override fun getAndUpdateUserRefreshesBetween(t1: LocalDateTime, t2: LocalDateTime): Int {
return brokenSitePromptInMemoryStore.getAndUpdateUserRefreshesBetween(t1, t2)
}

private fun convertToShortDate(dateString: String): String {
val inputFormatter = DateTimeFormatter.ISO_INSTANT
val outputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
Expand Down
Loading

0 comments on commit d248f95

Please sign in to comment.