Bug 1853301 - Refactor ReviewQualityCheckNavigationMiddleware to use TabsUseCases and SessionUseCases

Refactor LinkType class to support instances for each external link accessible from the review quality check feature
fenix/120.0
Alexandru2909 9 months ago committed by mergify[bot]
parent 354be91be7
commit 9cac2fca23

@ -16,8 +16,6 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.shopping.di.ReviewQualityCheckMiddlewareProvider
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
@ -36,13 +34,6 @@ class ReviewQualityCheckFragment : BottomSheetDialogFragment() {
settings = requireComponents.settings,
browserStore = requireComponents.core.store,
context = requireContext().applicationContext,
openLink = { link, shouldOpenInNewTab ->
(requireActivity() as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = link,
newTab = shouldOpenInNewTab,
from = BrowserDirection.FromReviewQualityCheck,
)
},
scope = lifecycleScope,
),
)

@ -7,6 +7,7 @@ package org.mozilla.fenix.shopping.di
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.shopping.middleware.DefaultNetworkChecker
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckPreferences
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckService
@ -27,21 +28,22 @@ object ReviewQualityCheckMiddlewareProvider {
* @param settings The [Settings] instance to use.
* @param browserStore The [BrowserStore] instance to access state.
* @param context The [Context] instance to use.
* @param openLink Opens a link. The callback is invoked with the URL [String] parameter and
* whether or not it should open in a new or the currently selected tab [Boolean] parameter.
* @param scope The [CoroutineScope] to use for launching coroutines.
*/
fun provideMiddleware(
settings: Settings,
browserStore: BrowserStore,
context: Context,
openLink: (String, Boolean) -> Unit,
scope: CoroutineScope,
): List<ReviewQualityCheckMiddleware> =
listOf(
providePreferencesMiddleware(settings, scope),
provideNetworkMiddleware(browserStore, context, scope),
provideNavigationMiddleware(openLink, scope),
provideNavigationMiddleware(
TabsUseCases.SelectOrAddUseCase(browserStore),
context,
scope,
),
)
private fun providePreferencesMiddleware(
@ -63,10 +65,12 @@ object ReviewQualityCheckMiddlewareProvider {
)
private fun provideNavigationMiddleware(
openLink: (String, Boolean) -> Unit,
selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
context: Context,
scope: CoroutineScope,
) = ReviewQualityCheckNavigationMiddleware(
openLink = openLink,
selectOrAddUseCase = selectOrAddUseCase,
context = context,
scope = scope,
)
}

@ -4,21 +4,28 @@
package org.mozilla.fenix.shopping.middleware
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
private const val POWERED_BY_URL = "www.fakespot.com"
/**
* Middleware that handles navigation events for the review quality check feature.
*
* @property openLink Callback used to open an url.
* @property selectOrAddUseCase UseCase instance used to open new tabs.
* @property context Context used to get SUMO urls.
* @property scope [CoroutineScope] used to launch coroutines.
*/
class ReviewQualityCheckNavigationMiddleware(
private val openLink: (String, Boolean) -> Unit,
private val selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
private val context: Context,
private val scope: CoroutineScope,
) : Middleware<ReviewQualityCheckState, ReviewQualityCheckAction> {
@ -39,13 +46,38 @@ class ReviewQualityCheckNavigationMiddleware(
private fun processAction(
action: ReviewQualityCheckAction.NavigationMiddlewareAction,
) = scope.launch {
when (action) {
is ReviewQualityCheckAction.OpenLink -> {
when (action.link) {
is ReviewQualityCheckState.LinkType.ExternalLink -> openLink(action.link.url, true)
is ReviewQualityCheckState.LinkType.AnalyzeLink -> openLink(action.link.url, false)
}
}
}
selectOrAddUseCase.invoke(actionToUrl(action))
}
/**
* Used to find the corresponding url to the open link action.
*
* @param action Used to find the corresponding url.
*/
private fun actionToUrl(
action: ReviewQualityCheckAction.NavigationMiddlewareAction,
) = when (action) {
// Placeholder SUMO urls to be used until the Fakespot SUMO pages are added in 1854277
is ReviewQualityCheckAction.OpenExplainerLearnMoreLink -> SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.HELP,
)
is ReviewQualityCheckAction.OpenOnboardingTermsLink -> SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.HELP,
)
is ReviewQualityCheckAction.OpenOnboardingLearnMoreLink -> SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.HELP,
)
is ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink -> SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.HELP,
)
is ReviewQualityCheckAction.OpenPoweredByLink -> POWERED_BY_URL
}
}

@ -85,7 +85,27 @@ sealed interface ReviewQualityCheckAction : Action {
object ReanalyzeProduct : NetworkAction, UpdateAction
/**
* Triggered when opening a link from the review quality check feature.
* Triggered when the user clicks on learn more link on the explainer card.
*/
data class OpenLink(val link: ReviewQualityCheckState.LinkType) : NavigationMiddlewareAction
object OpenExplainerLearnMoreLink : NavigationMiddlewareAction
/**
* Triggered when the user clicks on the "Powered by" link in the footer.
*/
object OpenPoweredByLink : NavigationMiddlewareAction
/**
* Triggered when the user clicks on learn more link on the opt in card.
*/
object OpenOnboardingLearnMoreLink : NavigationMiddlewareAction
/**
* Triggered when the user clicks on terms and conditions link on the opt in card.
*/
object OpenOnboardingTermsLink : NavigationMiddlewareAction
/**
* Triggered when the user clicks on privacy policy link on the opt in card.
*/
object OpenOnboardingPrivacyPolicyLink : NavigationMiddlewareAction
}

@ -151,21 +151,6 @@ sealed interface ReviewQualityCheckState : State {
QUALITY, PRICE, SHIPPING, PACKAGING_AND_APPEARANCE, COMPETITIVENESS
}
/**
* Types of links that can be opened from the review quality check feature.
*/
sealed class LinkType {
/**
* Opens a link to analyze a product.
*/
data class AnalyzeLink(val url: String) : LinkType()
/**
* Opens an external "Learn more" link.
*/
data class ExternalLink(val url: String) : LinkType()
}
/**
* The state of the recommended product.
*/

@ -52,7 +52,7 @@ fun NoAnalysis(
isAnalyzing: Boolean,
productRecommendationsEnabled: Boolean?,
onAnalyzeClick: () -> Unit,
onReviewGradeLearnMoreClick: (String) -> Unit,
onReviewGradeLearnMoreClick: () -> Unit,
onOptOutClick: () -> Unit,
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,

@ -66,8 +66,8 @@ fun ProductAnalysis(
onOptOutClick: () -> Unit,
onReanalyzeClick: () -> Unit,
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
onReviewGradeLearnMoreClick: (String) -> Unit,
onFooterLinkClick: (String) -> Unit,
onReviewGradeLearnMoreClick: () -> Unit,
onFooterLinkClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(

@ -37,10 +37,10 @@ import org.mozilla.fenix.theme.FirefoxTheme
fun ProductAnalysisError(
error: ProductReviewState.Error,
productRecommendationsEnabled: Boolean?,
onReviewGradeLearnMoreClick: (String) -> Unit,
onReviewGradeLearnMoreClick: () -> Unit,
onOptOutClick: () -> Unit,
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
onFooterLinkClick: (String) -> Unit,
onFooterLinkClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(

@ -25,7 +25,6 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
* @param onRequestDismiss Invoked when a user action requests dismissal of the bottom sheet.
* @param modifier The modifier to be applied to the Composable.
*/
@Suppress("LongMethod")
@Composable
fun ReviewQualityCheckBottomSheet(
store: ReviewQualityCheckStore,
@ -47,26 +46,17 @@ fun ReviewQualityCheckBottomSheet(
onPrimaryButtonClick = {
store.dispatch(ReviewQualityCheckAction.OptIn)
},
onLearnMoreClick = { url ->
store.dispatch(
ReviewQualityCheckAction.OpenLink(
ReviewQualityCheckState.LinkType.ExternalLink(url),
),
)
onLearnMoreClick = {
onRequestDismiss()
store.dispatch(ReviewQualityCheckAction.OpenOnboardingLearnMoreLink)
},
onPrivacyPolicyClick = { url ->
store.dispatch(
ReviewQualityCheckAction.OpenLink(
ReviewQualityCheckState.LinkType.ExternalLink(url),
),
)
onPrivacyPolicyClick = {
onRequestDismiss()
store.dispatch(ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink)
},
onTermsOfUseClick = { url ->
store.dispatch(
ReviewQualityCheckAction.OpenLink(
ReviewQualityCheckState.LinkType.ExternalLink(url),
),
)
onTermsOfUseClick = {
onRequestDismiss()
store.dispatch(ReviewQualityCheckAction.OpenOnboardingTermsLink)
},
onSecondaryButtonClick = onRequestDismiss,
)
@ -85,19 +75,13 @@ fun ReviewQualityCheckBottomSheet(
onProductRecommendationsEnabledStateChange = {
store.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation)
},
onReviewGradeLearnMoreClick = { url ->
store.dispatch(
ReviewQualityCheckAction.OpenLink(
ReviewQualityCheckState.LinkType.ExternalLink(url),
),
)
onReviewGradeLearnMoreClick = {
onRequestDismiss()
store.dispatch(ReviewQualityCheckAction.OpenExplainerLearnMoreLink)
},
onFooterLinkClick = { url ->
store.dispatch(
ReviewQualityCheckAction.OpenLink(
ReviewQualityCheckState.LinkType.ExternalLink(url),
),
)
onFooterLinkClick = {
onRequestDismiss()
store.dispatch(ReviewQualityCheckAction.OpenPoweredByLink)
},
)
}
@ -120,8 +104,8 @@ private fun ProductReview(
onOptOutClick: () -> Unit,
onReanalyzeClick: () -> Unit,
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
onReviewGradeLearnMoreClick: (String) -> Unit,
onFooterLinkClick: (String) -> Unit,
onReviewGradeLearnMoreClick: () -> Unit,
onFooterLinkClick: () -> Unit,
) {
Crossfade(
targetState = state.productReviewState,

@ -45,9 +45,9 @@ const val PLACEHOLDER_URL = "www.fakespot.com"
@Composable
fun ReviewQualityCheckContextualOnboarding(
retailers: List<ProductVendor>,
onLearnMoreClick: (String) -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
onTermsOfUseClick: (String) -> Unit,
onLearnMoreClick: () -> Unit,
onPrivacyPolicyClick: () -> Unit,
onTermsOfUseClick: () -> Unit,
onPrimaryButtonClick: () -> Unit,
onSecondaryButtonClick: () -> Unit,
) {
@ -85,7 +85,9 @@ fun ReviewQualityCheckContextualOnboarding(
LinkTextState(
text = learnMoreText,
url = PLACEHOLDER_URL,
onClick = onLearnMoreClick,
onClick = {
onLearnMoreClick()
},
),
),
style = FirefoxTheme.typography.body2.copy(
@ -107,12 +109,16 @@ fun ReviewQualityCheckContextualOnboarding(
LinkTextState(
text = privacyPolicyText,
url = PLACEHOLDER_URL,
onClick = onPrivacyPolicyClick,
onClick = {
onPrivacyPolicyClick()
},
),
LinkTextState(
text = termsOfUseText,
url = PLACEHOLDER_URL,
onClick = onTermsOfUseClick,
onClick = {
onTermsOfUseClick()
},
),
),
style = FirefoxTheme.typography.caption

@ -18,8 +18,6 @@ import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
private const val FOOTER_LINK = "http://fakespot.com/"
/**
* Review Quality Check footer with an embedded link to navigate to Fakespot.com.
*
@ -27,7 +25,7 @@ private const val FOOTER_LINK = "http://fakespot.com/"
*/
@Composable
fun ReviewQualityCheckFooter(
onLinkClick: (String) -> Unit,
onLinkClick: () -> Unit,
) {
val poweredByLinkText = stringResource(
id = R.string.review_quality_check_powered_by_link,
@ -42,8 +40,10 @@ fun ReviewQualityCheckFooter(
linkTextStates = listOf(
LinkTextState(
text = poweredByLinkText,
url = FOOTER_LINK,
onClick = onLinkClick,
url = "",
onClick = {
onLinkClick()
},
),
),
style = FirefoxTheme.typography.body2.copy(

@ -18,7 +18,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
@ -26,7 +25,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ClickableSubstringLink
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.parseHtml
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
import org.mozilla.fenix.theme.FirefoxTheme
@ -39,7 +37,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
@Composable
fun ReviewQualityInfoCard(
modifier: Modifier = Modifier,
onLearnMoreClick: (String) -> Unit,
onLearnMoreClick: () -> Unit,
) {
ReviewQualityCheckExpandableCard(
title = stringResource(id = R.string.review_quality_check_explanation_title),
@ -56,7 +54,7 @@ fun ReviewQualityInfoCard(
@Composable
private fun ReviewQualityInfo(
modifier: Modifier = Modifier,
onLearnMoreClick: (String) -> Unit,
onLearnMoreClick: () -> Unit,
) {
Column(
modifier = modifier,
@ -81,7 +79,6 @@ private fun ReviewQualityInfo(
stringResource(R.string.shopping_product_name),
)
val text = stringResource(R.string.review_quality_check_info_learn_more, link)
val context = LocalContext.current
val linkStartIndex = text.indexOf(link)
val linkEndIndex = linkStartIndex + link.length
ClickableSubstringLink(
@ -89,15 +86,7 @@ private fun ReviewQualityInfo(
textStyle = FirefoxTheme.typography.body2,
clickableStartIndex = linkStartIndex,
clickableEndIndex = linkEndIndex,
onClick = {
onLearnMoreClick(
// Placeholder Sumo page
SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.HELP,
),
)
},
onClick = onLearnMoreClick,
)
ReviewGradingScaleInfo(

@ -1,75 +1,56 @@
package org.mozilla.fenix.shopping.middleware
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
@RunWith(FenixRobolectricTestRunner::class)
class ReviewQualityCheckNavigationMiddlewareTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val dispatcher = coroutinesTestRule.testDispatcher
private val scope = coroutinesTestRule.scope
@Test
fun `WHEN opening an external link THEN the link should be opened in a new tab`() {
var isOpenedInSelectedTab = false
var isOpenedInNewTab = false
val store = ReviewQualityCheckStore(
middleware = listOf(
ReviewQualityCheckNavigationMiddleware(
openLink = { _, openInNewTab ->
if (openInNewTab) {
isOpenedInNewTab = true
} else {
isOpenedInSelectedTab = true
}
},
scope = scope,
),
),
private lateinit var store: ReviewQualityCheckStore
private lateinit var browserStore: BrowserStore
private lateinit var addTabUseCase: TabsUseCases.SelectOrAddUseCase
private lateinit var middleware: ReviewQualityCheckNavigationMiddleware
@Before
fun setup() {
browserStore = BrowserStore()
addTabUseCase = TabsUseCases.SelectOrAddUseCase(browserStore)
middleware = ReviewQualityCheckNavigationMiddleware(
selectOrAddUseCase = addTabUseCase,
context = testContext,
scope = scope,
)
store = ReviewQualityCheckStore(
middleware = listOf(middleware),
)
store.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
store.dispatch(ReviewQualityCheckAction.OpenLink(ReviewQualityCheckState.LinkType.ExternalLink("www.mozilla.com"))).joinBlocking()
store.waitUntilIdle()
assertEquals(true, isOpenedInNewTab)
assertEquals(false, isOpenedInSelectedTab)
}
@Test
fun `WHEN re-analzying a product THEN the link should be opened in the currently selected tab`() {
var isOpenedInSelectedTab = false
var isOpenedInNewTab = false
val store = ReviewQualityCheckStore(
middleware = listOf(
ReviewQualityCheckNavigationMiddleware(
openLink = { _, openInNewTab ->
if (openInNewTab) {
isOpenedInNewTab = true
} else {
isOpenedInSelectedTab = true
}
},
scope = scope,
),
),
)
fun `WHEN opening an external link THEN the link should be opened in a new tab`() {
val action = ReviewQualityCheckAction.OpenPoweredByLink
store.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
assertEquals(0, browserStore.state.tabs.size)
store.dispatch(ReviewQualityCheckAction.OpenLink(ReviewQualityCheckState.LinkType.AnalyzeLink("www.mozilla.com"))).joinBlocking()
store.dispatch(action).joinBlocking()
store.waitUntilIdle()
assertEquals(true, isOpenedInSelectedTab)
assertEquals(false, isOpenedInNewTab)
assertEquals(1, browserStore.state.tabs.size)
}
}

Loading…
Cancel
Save