Bug 1845260 - Create top level structure for Review Quality Check

fenix/117.0
rahulsainani 10 months ago committed by mergify[bot]
parent b7ff198e44
commit 609895be91

@ -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<View>? = 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
},

@ -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,
)

@ -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
},
)
}
}
}

@ -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
}
}
}

@ -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),
)
}

@ -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(),
)
}
}
}

@ -118,6 +118,14 @@
<string name="review_quality_check_grade_a_b_description" translatable="false">Reliable reviews</string>
<string name="review_quality_check_grade_c_description" translatable="false">Only some reliable reviews</string>
<string name="review_quality_check_grade_d_f_description" translatable="false">Unreliable reviews</string>
<string name="review_quality_check_star_rating_content_description">%1$s out of 5 stars</string>
<string name="review_quality_check_grade_title">How reliable are the reviews?</string>
<string name="review_quality_check_star_rating_content_description" translatable="false">%1$s out of 5 stars</string>
<string name="review_quality_check_grade_title" translatable="false">How reliable are the reviews?</string>
<string name="review_quality_check_contextual_onboarding_title" translatable="false">Shop based on real reviews</string>
<string name="review_quality_check_contextual_onboarding_description" translatable="false">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, its all powered by AI technology from Fakespot, a popular browser extension thats now built in.</string>
<string name="review_quality_check_contextual_onboarding_primary_button_text" translatable="false">Yes, check review quality</string>
<string name="review_quality_check_contextual_onboarding_secondary_button_text" translatable="false">Not now</string>
<string name="review_quality_check_contextual_onboarding_caption" translatable="false">Analysis powered by Fakespot.com. Learn more.</string>
<string name="review_quality_check_settings_title" translatable="false">Settings</string>
<string name="review_quality_check_settings_recommended_products" translatable="false">Show products recommended by Firefox</string>
<string name="review_quality_check_settings_turn_off" translatable="false">Turn off review quality check</string>
</resources>

Loading…
Cancel
Save