diff --git a/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt b/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt index 3f73a4a2a6..1de585aa9d 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt @@ -11,6 +11,7 @@ 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 +import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckVendorsService import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNavigationMiddleware import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNetworkMiddleware import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckPreferencesMiddleware @@ -37,7 +38,7 @@ object ReviewQualityCheckMiddlewareProvider { scope: CoroutineScope, ): List = listOf( - providePreferencesMiddleware(settings, scope), + providePreferencesMiddleware(settings, browserStore, scope), provideNetworkMiddleware(browserStore, context, scope), provideNavigationMiddleware( TabsUseCases.SelectOrAddUseCase(browserStore), @@ -48,9 +49,11 @@ object ReviewQualityCheckMiddlewareProvider { private fun providePreferencesMiddleware( settings: Settings, + browserStore: BrowserStore, scope: CoroutineScope, ) = ReviewQualityCheckPreferencesMiddleware( reviewQualityCheckPreferences = DefaultReviewQualityCheckPreferences(settings), + reviewQualityCheckVendorsService = DefaultReviewQualityCheckVendorsService(browserStore), scope = scope, ) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt index 8a34090248..14ac950fce 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt @@ -20,6 +20,7 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState */ class ReviewQualityCheckPreferencesMiddleware( private val reviewQualityCheckPreferences: ReviewQualityCheckPreferences, + private val reviewQualityCheckVendorsService: ReviewQualityCheckVendorsService, private val scope: CoroutineScope, ) : ReviewQualityCheckMiddleware { @@ -50,12 +51,14 @@ class ReviewQualityCheckPreferencesMiddleware( val hasUserOptedIn = reviewQualityCheckPreferences.enabled() val isProductRecommendationsEnabled = reviewQualityCheckPreferences.productRecommendationsEnabled() - store.dispatch( - ReviewQualityCheckAction.UpdateUserPreferences( - hasUserOptedIn = hasUserOptedIn, - isProductRecommendationsEnabled = isProductRecommendationsEnabled, - ), - ) + + val updateUserPreferences = if (hasUserOptedIn) { + ReviewQualityCheckAction.OptInCompleted(isProductRecommendationsEnabled) + } else { + val productVendors = reviewQualityCheckVendorsService.productVendors() + ReviewQualityCheckAction.OptOutCompleted(productVendors) + } + store.dispatch(updateUserPreferences) } } @@ -64,10 +67,7 @@ class ReviewQualityCheckPreferencesMiddleware( val isProductRecommendationsEnabled = reviewQualityCheckPreferences.productRecommendationsEnabled() store.dispatch( - ReviewQualityCheckAction.UpdateUserPreferences( - hasUserOptedIn = true, - isProductRecommendationsEnabled = isProductRecommendationsEnabled, - ), + ReviewQualityCheckAction.OptInCompleted(isProductRecommendationsEnabled), ) // Update the preference diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt new file mode 100644 index 0000000000..3e1decb948 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.shopping.middleware + +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor +import java.net.URI +import java.net.URISyntaxException + +private const val AMAZON_COM = "amazon.com" +private const val BEST_BUY_COM = "bestbuy.com" +private const val WALMART_COM = "walmart.com" +private val defaultVendorsList = enumValues().toList() + +/** + * Service for getting the list of product vendors. + */ +interface ReviewQualityCheckVendorsService { + + /** + * Returns the list of product vendors in order. + */ + fun productVendors(): List +} + +/** + * Default implementation of [ReviewQualityCheckVendorsService] that uses the [BrowserStore] to + * identify the selected tab. + * + * @property browserStore The [BrowserStore] instance to use. + */ +class DefaultReviewQualityCheckVendorsService( + private val browserStore: BrowserStore, +) : ReviewQualityCheckVendorsService { + + override fun productVendors(): List { + val selectedTabUrl = browserStore.state.selectedTab?.content?.url + + return if (selectedTabUrl == null) { + defaultVendorsList + } else { + val host = selectedTabUrl.toJavaUri()?.host + when { + host == null -> defaultVendorsList + host.contains(AMAZON_COM) -> createProductVendorsList(ProductVendor.AMAZON) + host.contains(BEST_BUY_COM) -> createProductVendorsList(ProductVendor.BEST_BUY) + host.contains(WALMART_COM) -> createProductVendorsList(ProductVendor.WALMART) + else -> defaultVendorsList + } + } + } + + /** + * Creates list of product vendors using the firstVendor param as the first item in the list. + */ + private fun createProductVendorsList(firstVendor: ProductVendor): List = + listOf(firstVendor) + defaultVendorsList.filterNot { it == firstVendor } + + /** + * Convenience function to converts a given string to a [URI] instance. Returns null if the + * string is not a valid URI. + */ + private fun String.toJavaUri(): URI? { + return try { + URI.create(this) + } catch (e: URISyntaxException) { + Logger.error("Unable to create URI with the given string $this", e) + null + } catch (e: IllegalArgumentException) { + Logger.error("Unable to create URI with the given string $this", e) + null + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt index 7bad4be15a..796881acb0 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt @@ -53,15 +53,20 @@ sealed interface ReviewQualityCheckAction : Action { object ToggleProductRecommendation : PreferencesMiddlewareAction, UpdateAction /** - * Triggered as a result of a [PreferencesMiddlewareAction] to update the state. + * Triggered as a result of a [OptIn] or [Init] whe user has opted in for shopping experience. * - * @property hasUserOptedIn True when user has opted in for shopping experience. * @property isProductRecommendationsEnabled Reflects the user preference update to display * recommended product. Null when product recommendations feature is disabled. */ - data class UpdateUserPreferences( - val hasUserOptedIn: Boolean, - val isProductRecommendationsEnabled: Boolean?, + data class OptInCompleted(val isProductRecommendationsEnabled: Boolean?) : UpdateAction + + /** + * Triggered as a result of [Init] when user has opted out of shopping experience. + * + * @property productVendors List of product vendors in relevant order. + */ + data class OptOutCompleted( + val productVendors: List, ) : UpdateAction /** diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt index 315eec5f50..b4cdbd7120 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt @@ -22,14 +22,10 @@ sealed interface ReviewQualityCheckState : State { /** * The state when the user has not opted in for the feature. * - * @property retailers List of retailer names to be displayed in order in the onboarding UI. + * @property productVendors List of vendors to be displayed in order in the onboarding UI. */ data class NotOptedIn( - val retailers: List = listOf( - ProductVendor.AMAZON, - ProductVendor.BEST_BUY, - ProductVendor.WALMART, - ), + val productVendors: List = enumValues().toList(), ) : ReviewQualityCheckState /** diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt index c7d44ac1b4..c6ac64de77 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt @@ -41,19 +41,18 @@ private fun mapStateForUpdateAction( action: ReviewQualityCheckAction.UpdateAction, ): ReviewQualityCheckState { return when (action) { - is ReviewQualityCheckAction.UpdateUserPreferences -> { - if (action.hasUserOptedIn) { - if (state is ReviewQualityCheckState.OptedIn) { - state.copy(productRecommendationsPreference = action.isProductRecommendationsEnabled) - } else { - ReviewQualityCheckState.OptedIn( - productRecommendationsPreference = action.isProductRecommendationsEnabled, - ) - } + is ReviewQualityCheckAction.OptInCompleted -> { + if (state is ReviewQualityCheckState.OptedIn) { + state.copy(productRecommendationsPreference = action.isProductRecommendationsEnabled) } else { - ReviewQualityCheckState.NotOptedIn() + ReviewQualityCheckState.OptedIn( + productRecommendationsPreference = action.isProductRecommendationsEnabled, + ) } } + is ReviewQualityCheckAction.OptOutCompleted -> { + ReviewQualityCheckState.NotOptedIn(action.productVendors) + } ReviewQualityCheckAction.OptOut -> { ReviewQualityCheckState.NotOptedIn() diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt index ab3878acd1..e90a0b0547 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt @@ -42,7 +42,7 @@ fun ReviewQualityCheckBottomSheet( when (val state = reviewQualityCheckState) { is ReviewQualityCheckState.NotOptedIn -> { ReviewQualityCheckContextualOnboarding( - retailers = state.retailers, + productVendors = state.productVendors, onPrimaryButtonClick = { store.dispatch(ReviewQualityCheckAction.OptIn) }, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt index 97fc43a343..7d4a4f5868 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt @@ -34,7 +34,7 @@ const val PLACEHOLDER_URL = "www.fakespot.com" * A placeholder UI for review quality check contextual onboarding. The actual UI will be * implemented as part of Bug 1840103 with the illustration. * - * @param retailers List of retailers to be displayed in order. + * @param productVendors List of retailers to be displayed in order. * @param onLearnMoreClick Invoked when a user clicks on the learn more link. * @param onPrivacyPolicyClick Invoked when a user clicks on the privacy policy link. * @param onTermsOfUseClick Invoked when a user clicks on the terms of use link. @@ -44,7 +44,7 @@ const val PLACEHOLDER_URL = "www.fakespot.com" @Suppress("LongParameterList", "LongMethod") @Composable fun ReviewQualityCheckContextualOnboarding( - retailers: List, + productVendors: List, onLearnMoreClick: () -> Unit, onPrivacyPolicyClick: () -> Unit, onTermsOfUseClick: () -> Unit, @@ -68,7 +68,7 @@ fun ReviewQualityCheckContextualOnboarding( Spacer(modifier = Modifier.height(16.dp)) Text( - text = createDescriptionString(retailers), + text = createDescriptionString(productVendors), color = FirefoxTheme.colors.textSecondary, style = FirefoxTheme.typography.body2, ) diff --git a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeNetworkChecker.kt b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeNetworkChecker.kt index 2e3750b774..7f5d739e16 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeNetworkChecker.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeNetworkChecker.kt @@ -7,7 +7,7 @@ package org.mozilla.fenix.shopping.fake import org.mozilla.fenix.shopping.middleware.NetworkChecker class FakeNetworkChecker( - private val isConnected: Boolean, + private val isConnected: Boolean = true, ) : NetworkChecker { override fun isConnected(): Boolean = isConnected } diff --git a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckVendorsService.kt b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckVendorsService.kt new file mode 100644 index 0000000000..9d1caaa349 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckVendorsService.kt @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.shopping.fake + +import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckVendorsService +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor + +class FakeReviewQualityCheckVendorsService( + private val productVendors: List = listOf( + ProductVendor.BEST_BUY, + ProductVendor.WALMART, + ProductVendor.AMAZON, + ), +) : ReviewQualityCheckVendorsService { + override fun productVendors(): List = productVendors +} diff --git a/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckVendorsServiceTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckVendorsServiceTest.kt new file mode 100644 index 0000000000..362f242d9d --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckVendorsServiceTest.kt @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.shopping.middleware + +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor + +class DefaultReviewQualityCheckVendorsServiceTest { + + @Test + fun `WHEN selected tab is an amazon page THEN amazon is first in product vendors list`() = + runTest { + val tab = createTab( + url = "https://www.amazon.com/product", + id = "test-tab", + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState)) + + val actual = tested.productVendors() + val expected = listOf( + ProductVendor.AMAZON, + ProductVendor.BEST_BUY, + ProductVendor.WALMART, + ) + + assertEquals(expected, actual) + } + + @Test + fun `WHEN selected tab is a walmart page THEN walmart is first in product vendors list`() = + runTest { + val tab = createTab( + url = "https://www.walmart.com/product", + id = "test-tab", + ) + + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState)) + + val actual = tested.productVendors() + val expected = listOf( + ProductVendor.WALMART, + ProductVendor.AMAZON, + ProductVendor.BEST_BUY, + ) + + assertEquals(expected, actual) + } + + @Test + fun `WHEN selected tab is a best buy page THEN best buy is first in product vendors list`() = + runTest { + val tab = createTab( + url = "https://www.bestbuy.com/product", + id = "test-tab", + ) + + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState)) + + val actual = tested.productVendors() + val expected = listOf( + ProductVendor.BEST_BUY, + ProductVendor.AMAZON, + ProductVendor.WALMART, + ) + + assertEquals(expected, actual) + } + + @Test + fun `WHEN selected tab is a not a vendor page THEN default product vendors list is returned`() = + runTest { + val tab = createTab( + url = "https://www.shopping.xyz/product", + id = "test-tab", + ) + + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState)) + + val actual = tested.productVendors() + val expected = listOf( + ProductVendor.AMAZON, + ProductVendor.BEST_BUY, + ProductVendor.WALMART, + ) + + assertEquals(expected, actual) + } + + @Test + fun `WHEN selected tab is a not a valid uri THEN default product vendors list is returned`() = + runTest { + val tab = createTab( + url = "not a url", + id = "test-tab", + ) + + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState)) + + val actual = tested.productVendors() + val expected = listOf( + ProductVendor.AMAZON, + ProductVendor.BEST_BUY, + ProductVendor.WALMART, + ) + + assertEquals(expected, actual) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt index 348ffe976f..d108bf6b34 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt @@ -15,6 +15,7 @@ import org.mozilla.fenix.shopping.ProductAnalysisTestData import org.mozilla.fenix.shopping.fake.FakeNetworkChecker import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckPreferences import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckService +import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckVendorsService import org.mozilla.fenix.shopping.middleware.AnalysisStatusDto import org.mozilla.fenix.shopping.middleware.NetworkChecker import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNetworkMiddleware @@ -22,6 +23,7 @@ import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckPreferences import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckPreferencesMiddleware import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckService import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor class ReviewQualityCheckStoreTest { @@ -39,13 +41,26 @@ class ReviewQualityCheckStoreTest { isEnabled = false, isProductRecommendationsEnabled = false, ), + reviewQualityCheckVendorsService = FakeReviewQualityCheckVendorsService( + productVendors = listOf( + ProductVendor.BEST_BUY, + ProductVendor.AMAZON, + ProductVendor.WALMART, + ), + ), ), ) tested.waitUntilIdle() dispatcher.scheduler.advanceUntilIdle() tested.waitUntilIdle() - val expected = ReviewQualityCheckState.NotOptedIn() + val expected = ReviewQualityCheckState.NotOptedIn( + productVendors = listOf( + ProductVendor.BEST_BUY, + ProductVendor.AMAZON, + ProductVendor.WALMART, + ), + ) assertEquals(expected, tested.state) } @@ -331,29 +346,22 @@ class ReviewQualityCheckStoreTest { } private fun provideReviewQualityCheckMiddleware( - reviewQualityCheckPreferences: ReviewQualityCheckPreferences, - reviewQualityCheckService: ReviewQualityCheckService? = null, - networkChecker: NetworkChecker? = null, + reviewQualityCheckPreferences: ReviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(), + reviewQualityCheckVendorsService: FakeReviewQualityCheckVendorsService = FakeReviewQualityCheckVendorsService(), + reviewQualityCheckService: ReviewQualityCheckService = FakeReviewQualityCheckService(), + networkChecker: NetworkChecker = FakeNetworkChecker(), ): List { - return if (reviewQualityCheckService != null && networkChecker != null) { - listOf( - ReviewQualityCheckPreferencesMiddleware( - reviewQualityCheckPreferences = reviewQualityCheckPreferences, - scope = this.scope, - ), - ReviewQualityCheckNetworkMiddleware( - reviewQualityCheckService = reviewQualityCheckService, - networkChecker = networkChecker, - scope = this.scope, - ), - ) - } else { - listOf( - ReviewQualityCheckPreferencesMiddleware( - reviewQualityCheckPreferences = reviewQualityCheckPreferences, - scope = this.scope, - ), - ) - } + return listOf( + ReviewQualityCheckPreferencesMiddleware( + reviewQualityCheckPreferences = reviewQualityCheckPreferences, + reviewQualityCheckVendorsService = reviewQualityCheckVendorsService, + scope = this.scope, + ), + ReviewQualityCheckNetworkMiddleware( + reviewQualityCheckService = reviewQualityCheckService, + networkChecker = networkChecker, + scope = this.scope, + ), + ) } }