From 609895be91e2c73cef533781e9cb4c24dce5dda5 Mon Sep 17 00:00:00 2001 From: rahulsainani Date: Tue, 25 Jul 2023 11:53:49 +0200 Subject: [PATCH] Bug 1845260 - Create top level structure for Review Quality Check --- .../shopping/ReviewQualityCheckFragment.kt | 17 +- .../shopping/state/ReviewQualityCheckState.kt | 18 +- .../fenix/shopping/ui/ProductAnalysis.kt | 164 ++++++++++++++++++ .../ui/ReviewQualityCheckBottomSheet.kt | 92 ++++++++++ .../ReviewQualityCheckContextualOnboarding.kt | 79 +++++++++ ...ntent.kt => ReviewQualityCheckScaffold.kt} | 61 ++----- app/src/main/res/values/static_strings.xml | 12 +- 7 files changed, 390 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt create mode 100644 app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt create mode 100644 app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt rename app/src/main/java/org/mozilla/fenix/shopping/ui/{ReviewQualityCheckContent.kt => ReviewQualityCheckScaffold.kt} (65%) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt index 18714750c7..eec49a12af 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt @@ -13,9 +13,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.ComposeView 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.shopping.ui.ReviewQualityCheckContent +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.shopping.state.ReviewQualityCheckPreferencesImpl +import org.mozilla.fenix.shopping.state.ReviewQualityCheckStore +import org.mozilla.fenix.shopping.ui.ReviewQualityCheckBottomSheet import org.mozilla.fenix.theme.FirefoxTheme /** @@ -24,6 +28,14 @@ import org.mozilla.fenix.theme.FirefoxTheme class ReviewQualityCheckFragment : BottomSheetDialogFragment() { private var behavior: BottomSheetBehavior? = null + private val store by lazy { + ReviewQualityCheckStore( + reviewQualityCheckPreferences = ReviewQualityCheckPreferencesImpl( + requireComponents.settings, + ), + scope = lifecycleScope, + ) + } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = super.onCreateDialog(savedInstanceState).apply { @@ -42,7 +54,8 @@ class ReviewQualityCheckFragment : BottomSheetDialogFragment() { ): View = ComposeView(requireContext()).apply { setContent { FirefoxTheme { - ReviewQualityCheckContent( + ReviewQualityCheckBottomSheet( + store = store, onRequestDismiss = { behavior?.state = BottomSheetBehavior.STATE_HIDDEN }, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/state/ReviewQualityCheckState.kt b/app/src/main/java/org/mozilla/fenix/shopping/state/ReviewQualityCheckState.kt index dcfe27ef02..4a3c426e46 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/state/ReviewQualityCheckState.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/state/ReviewQualityCheckState.kt @@ -28,7 +28,7 @@ sealed interface ReviewQualityCheckState : State { * recommendations. True if product recommendations should be shown. */ data class OptedIn( - val productReviewState: ProductReviewState = ProductReviewState.Loading, + val productReviewState: ProductReviewState = fakeAnalysis, val productRecommendationsPreference: Boolean, ) : ReviewQualityCheckState { @@ -56,7 +56,7 @@ sealed interface ReviewQualityCheckState : State { ) : ProductReviewState /** - * Denotes the state where analysis of the product is fetched and available. + * Denotes the state where analysis of the product is fetched and present. * * @property productId The id of the product, e.g ASIN, SKU. * @property reviewGrade The review grade of the product. @@ -67,7 +67,7 @@ sealed interface ReviewQualityCheckState : State { * @property highlights Optional highlights based on recent reviews of the product. * @property recommendedProductState The state of the recommended product. */ - data class ProductAnalysis( + data class AnalysisPresent( val productId: String, val reviewGrade: Grade, val needsAnalysis: Boolean, @@ -136,3 +136,15 @@ sealed interface ReviewQualityCheckState : State { ) : RecommendedProductState } } + +/** + * Fake analysis for showing the UI. To be deleted once the API is integrated. + */ +private val fakeAnalysis = ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent( + productId = "123", + reviewGrade = ReviewQualityCheckState.Grade.B, + needsAnalysis = false, + adjustedRating = 3.6f, + productUrl = "123", + highlights = null, +) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt new file mode 100644 index 0000000000..e14de77c6b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt @@ -0,0 +1,164 @@ +/* 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.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.SwitchWithLabel +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.button.SecondaryButton +import org.mozilla.fenix.shopping.state.ReviewQualityCheckState +import org.mozilla.fenix.shopping.state.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * UI for review quality check content displaying product analysis. + * + * @param productRecommendationsEnabled The current state of the product recommendations toggle. + * @param productAnalysis The product analysis to display. + * @param onOptOutClick Invoked when the user opts out of the review quality check feature. + * @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product + * recommendations toggle state. + * @param modifier The modifier to be applied to the Composable. + */ +@Composable +fun ProductAnalysis( + productRecommendationsEnabled: Boolean, + productAnalysis: AnalysisPresent, + onOptOutClick: () -> Unit, + onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ReviewGradeCard( + reviewGrade = productAnalysis.reviewGrade, + modifier = Modifier.fillMaxWidth(), + ) + + SettingsCard( + modifier = Modifier.fillMaxWidth(), + productRecommendationsEnabled = productRecommendationsEnabled, + onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, + onTurnOffReviewQualityCheckClick = onOptOutClick, + ) + } +} + +@Composable +private fun ReviewGradeCard( + reviewGrade: ReviewQualityCheckState.Grade, + modifier: Modifier = Modifier, +) { + ReviewQualityCheckCard(modifier = modifier.semantics(mergeDescendants = true) {}) { + Text( + text = stringResource(R.string.review_quality_check_grade_title), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline8, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + ReviewGradeExpanded(grade = reviewGrade) + } +} + +@Composable +private fun SettingsCard( + productRecommendationsEnabled: Boolean, + onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, + onTurnOffReviewQualityCheckClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ReviewQualityCheckExpandableCard( + modifier = modifier, + title = stringResource(R.string.review_quality_check_settings_title), + ) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + + SwitchWithLabel( + checked = productRecommendationsEnabled, + onCheckedChange = onProductRecommendationsEnabledStateChange, + label = stringResource(R.string.review_quality_check_settings_recommended_products), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SecondaryButton( + text = stringResource(R.string.review_quality_check_settings_turn_off), + onClick = onTurnOffReviewQualityCheckClick, + ) + } + } +} + +@Composable +@LightDarkPreview +private fun ProductAnalysisPreview() { + FirefoxTheme { + ReviewQualityCheckScaffold( + onRequestDismiss = {}, + ) { + val productRecommendationsEnabled = remember { mutableStateOf(false) } + + ProductAnalysis( + productRecommendationsEnabled = productRecommendationsEnabled.value, + productAnalysis = AnalysisPresent( + productId = "123", + reviewGrade = ReviewQualityCheckState.Grade.B, + needsAnalysis = false, + adjustedRating = 3.6f, + productUrl = "123", + highlights = mapOf( + ReviewQualityCheckState.HighlightType.QUALITY to listOf( + "High quality", + "Excellent craftsmanship", + "Superior materials", + ), + ReviewQualityCheckState.HighlightType.PRICE to listOf( + "Affordable prices", + "Great value for money", + "Discounted offers", + ), + ReviewQualityCheckState.HighlightType.SHIPPING to listOf( + "Fast and reliable shipping", + "Free shipping options", + "Express delivery", + ), + ReviewQualityCheckState.HighlightType.PACKAGING_AND_APPEARANCE to listOf( + "Elegant packaging", + "Attractive appearance", + "Beautiful design", + ), + ReviewQualityCheckState.HighlightType.COMPETITIVENESS to listOf( + "Competitive pricing", + "Strong market presence", + "Unbeatable deals", + ), + ), + ), + onOptOutClick = {}, + onProductRecommendationsEnabledStateChange = { + productRecommendationsEnabled.value = it + }, + ) + } + } +} 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 new file mode 100644 index 0000000000..f7d750dba1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt @@ -0,0 +1,92 @@ +/* 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.ui + +import androidx.compose.animation.animateContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import mozilla.components.lib.state.ext.observeAsState +import org.mozilla.fenix.shopping.state.ReviewQualityCheckAction +import org.mozilla.fenix.shopping.state.ReviewQualityCheckState +import org.mozilla.fenix.shopping.state.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent +import org.mozilla.fenix.shopping.state.ReviewQualityCheckStore + +/** + * Top-level UI for the Review Quality Check feature. + * + * @param store [ReviewQualityCheckStore] that holds the state. + * @param onRequestDismiss Invoked when a user action requests dismissal of the bottom sheet. + * @param modifier The modifier to be applied to the Composable. + */ +@Composable +fun ReviewQualityCheckBottomSheet( + store: ReviewQualityCheckStore, + onRequestDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val reviewQualityCheckState by store.observeAsState(ReviewQualityCheckState.Initial) { it } + + ReviewQualityCheckScaffold( + onRequestDismiss = onRequestDismiss, + modifier = modifier.animateContentSize(), + ) { + when (val state = reviewQualityCheckState) { + is ReviewQualityCheckState.NotOptedIn -> { + ReviewQualityCheckContextualOnboarding( + onPrimaryButtonClick = { + store.dispatch(ReviewQualityCheckAction.OptIn) + }, + onSecondaryButtonClick = onRequestDismiss, + ) + } + + is ReviewQualityCheckState.OptedIn -> { + ProductReview( + state = state, + onOptOutClick = { + onRequestDismiss() + store.dispatch(ReviewQualityCheckAction.OptOut) + }, + onProductRecommendationsEnabledStateChange = { + store.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation) + }, + ) + } + + is ReviewQualityCheckState.Initial -> {} + } + } +} + +@Composable +private fun ProductReview( + state: ReviewQualityCheckState.OptedIn, + onOptOutClick: () -> Unit, + onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, +) { + when (val productReviewState = state.productReviewState) { + is AnalysisPresent -> { + ProductAnalysis( + productRecommendationsEnabled = state.productRecommendationsPreference, + productAnalysis = productReviewState, + onOptOutClick = onOptOutClick, + onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, + ) + } + + is ReviewQualityCheckState.OptedIn.ProductReviewState.Error -> { + // Bug 1840113 + } + + is ReviewQualityCheckState.OptedIn.ProductReviewState.Loading -> { + // Bug 1845255 + } + + is ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent -> { + // Bug 1840333 + } + } +} 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 new file mode 100644 index 0000000000..f3b349ce33 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt @@ -0,0 +1,79 @@ +/* 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.ui + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.button.PrimaryButton +import org.mozilla.fenix.compose.button.TextButton +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * A placeholder UI for review quality check contextual onboarding. The actual UI will be + * implemented as part of Bug 1840103 with the illustration. + * + * @param onPrimaryButtonClick Invoked when a user clicks on the primary button. + * @param onSecondaryButtonClick Invoked when a user clicks on the secondary button. + */ +@Composable +fun ColumnScope.ReviewQualityCheckContextualOnboarding( + onPrimaryButtonClick: () -> Unit, + onSecondaryButtonClick: () -> Unit, +) { + ReviewQualityCheckCard(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.review_quality_check_contextual_onboarding_title), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline5, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.review_quality_check_contextual_onboarding_description), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PrimaryButton( + text = stringResource(R.string.review_quality_check_contextual_onboarding_primary_button_text), + onClick = onPrimaryButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + text = stringResource(R.string.review_quality_check_contextual_onboarding_secondary_button_text), + onClick = onSecondaryButtonClick, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.review_quality_check_contextual_onboarding_caption), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.caption, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckScaffold.kt similarity index 65% rename from app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt rename to app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckScaffold.kt index 05bb3bfebd..e0614923ae 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckScaffold.kt @@ -6,17 +6,18 @@ package org.mozilla.fenix.shopping.ui import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -27,23 +28,24 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.compose.BottomSheetHandle -import org.mozilla.fenix.compose.annotation.LightDarkPreview -import org.mozilla.fenix.shopping.state.ReviewQualityCheckState import org.mozilla.fenix.theme.FirefoxTheme private val bottomSheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f /** - * Top-level UI for the Review Quality Check feature. + * A scaffold for review quality check UI that implements the basic layout structure with + * [BottomSheetHandle], [Header] and [content]. * - * @param onRequestDismiss Invoked when a user actions requests dismissal of the bottom sheet. + * @param onRequestDismiss Invoked when a user action requests dismissal of the bottom sheet. * @param modifier The modifier to be applied to the Composable. + * @param content The content of the bottom sheet. */ @Composable -fun ReviewQualityCheckContent( +fun ReviewQualityCheckScaffold( onRequestDismiss: () -> Unit, modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, ) { Column( modifier = modifier @@ -51,7 +53,11 @@ fun ReviewQualityCheckContent( color = FirefoxTheme.colors.layer1, shape = bottomSheetShape, ) - .padding(16.dp), + .verticalScroll(rememberScrollState()) + .padding( + vertical = 8.dp, + horizontal = 16.dp, + ), ) { BottomSheetHandle( onRequestDismiss = onRequestDismiss, @@ -67,10 +73,7 @@ fun ReviewQualityCheckContent( Spacer(modifier = Modifier.height(16.dp)) - ReviewGradeCard( - modifier = Modifier.fillMaxWidth(), - reviewGrade = ReviewQualityCheckState.Grade.B, - ) + content() Spacer(modifier = Modifier.height(16.dp)) } @@ -97,37 +100,3 @@ private fun Header() { ) } } - -@Composable -private fun ReviewGradeCard( - reviewGrade: ReviewQualityCheckState.Grade, - modifier: Modifier = Modifier, -) { - ReviewQualityCheckCard(modifier = modifier.semantics(mergeDescendants = true) {}) { - Text( - text = stringResource(R.string.review_quality_check_grade_title), - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.headline8, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - ReviewGradeExpanded(grade = reviewGrade) - } -} - -@Composable -@LightDarkPreview -private fun ReviewQualityCheckContentPreview() { - FirefoxTheme { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter, - ) { - ReviewQualityCheckContent( - onRequestDismiss = {}, - modifier = Modifier.fillMaxWidth(), - ) - } - } -} diff --git a/app/src/main/res/values/static_strings.xml b/app/src/main/res/values/static_strings.xml index fc892bd380..8d0cef1c15 100644 --- a/app/src/main/res/values/static_strings.xml +++ b/app/src/main/res/values/static_strings.xml @@ -118,6 +118,14 @@ Reliable reviews Only some reliable reviews Unreliable reviews - %1$s out of 5 stars - How reliable are the reviews? + %1$s out of 5 stars + How reliable are the reviews? + Shop based on real reviews + Review quality check is a new feature from Firefox. It helps you understand how reliable the reviews are for a product, when you shop on Amazon, Best Buy, and Walmart. \n\nAnd, it’s all powered by AI technology from Fakespot, a popular browser extension that’s now built in. + Yes, check review quality + Not now + Analysis powered by Fakespot.com. Learn more. + Settings + Show products recommended by Firefox + Turn off review quality check