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 d3abc7d89..32b6ddcdb 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 @@ -21,8 +21,23 @@ 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. */ - object NotOptedIn : ReviewQualityCheckState + data class NotOptedIn( + val retailers: List = listOf( + ProductVendor.AMAZON, + ProductVendor.BEST_BUY, + ProductVendor.WALMART, + ), + ) : ReviewQualityCheckState + + /** + * Supported product retailers. + */ + enum class ProductVendor { + AMAZON, BEST_BUY, WALMART, + } /** * The state when the user has opted in for the feature. 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 363f1f05b..c7d44ac1b 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 @@ -51,12 +51,12 @@ private fun mapStateForUpdateAction( ) } } else { - ReviewQualityCheckState.NotOptedIn + ReviewQualityCheckState.NotOptedIn() } } ReviewQualityCheckAction.OptOut -> { - ReviewQualityCheckState.NotOptedIn + ReviewQualityCheckState.NotOptedIn() } ReviewQualityCheckAction.ToggleProductRecommendation -> { 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 9bd65ea76..71a07d277 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 @@ -25,6 +25,7 @@ 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, @@ -42,9 +43,31 @@ fun ReviewQualityCheckBottomSheet( when (val state = reviewQualityCheckState) { is ReviewQualityCheckState.NotOptedIn -> { ReviewQualityCheckContextualOnboarding( + retailers = state.retailers, onPrimaryButtonClick = { store.dispatch(ReviewQualityCheckAction.OptIn) }, + onLearnMoreClick = { url -> + store.dispatch( + ReviewQualityCheckAction.OpenLink( + ReviewQualityCheckState.LinkType.ExternalLink(url), + ), + ) + }, + onPrivacyPolicyClick = { url -> + store.dispatch( + ReviewQualityCheckAction.OpenLink( + ReviewQualityCheckState.LinkType.ExternalLink(url), + ), + ) + }, + onTermsOfUseClick = { url -> + store.dispatch( + ReviewQualityCheckAction.OpenLink( + ReviewQualityCheckState.LinkType.ExternalLink(url), + ), + ) + }, onSecondaryButtonClick = onRequestDismiss, ) } 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 c92e804d3..a9fb8eaa1 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 @@ -4,71 +4,136 @@ package org.mozilla.fenix.shopping.ui -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import org.mozilla.fenix.R +import org.mozilla.fenix.compose.LinkText +import org.mozilla.fenix.compose.LinkTextState import org.mozilla.fenix.compose.button.PrimaryButton -import org.mozilla.fenix.compose.button.TextButton +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor import org.mozilla.fenix.theme.FirefoxTheme +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 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. * @param onPrimaryButtonClick Invoked when a user clicks on the primary button. * @param onSecondaryButtonClick Invoked when a user clicks on the secondary button. */ +@Suppress("LongParameterList", "LongMethod") @Composable -fun ColumnScope.ReviewQualityCheckContextualOnboarding( +fun ReviewQualityCheckContextualOnboarding( + retailers: List, + onLearnMoreClick: (String) -> Unit, + onPrivacyPolicyClick: (String) -> Unit, + onTermsOfUseClick: (String) -> Unit, onPrimaryButtonClick: () -> Unit, onSecondaryButtonClick: () -> Unit, ) { + val learnMoreText = + stringResource(id = R.string.review_quality_check_contextual_onboarding_learn_more_link) + val privacyPolicyText = + stringResource(id = R.string.review_quality_check_contextual_onboarding_privacy_policy) + val termsOfUseText = + stringResource(id = R.string.review_quality_check_contextual_onboarding_terms_use) + 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 = createDescriptionString(), - color = FirefoxTheme.colors.textPrimary, + text = createDescriptionString(retailers), + color = FirefoxTheme.colors.textSecondary, style = FirefoxTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.CenterHorizontally), ) Spacer(modifier = Modifier.height(12.dp)) - Text( + LinkText( text = stringResource( id = R.string.review_quality_check_contextual_onboarding_learn_more, stringResource(id = R.string.shopping_product_name), - stringResource(id = R.string.review_quality_check_contextual_onboarding_learn_more_link), + learnMoreText, ), - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.CenterHorizontally), + linkTextStates = listOf( + LinkTextState( + text = learnMoreText, + url = PLACEHOLDER_URL, + onClick = onLearnMoreClick, + ), + ), + style = FirefoxTheme.typography.body2.copy( + color = FirefoxTheme.colors.textSecondary, + ), + linkTextDecoration = TextDecoration.Underline, ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(12.dp)) + + LinkText( + text = stringResource( + id = R.string.review_quality_check_contextual_onboarding_caption, + stringResource(id = R.string.shopping_product_name), + privacyPolicyText, + termsOfUseText, + ), + linkTextStates = listOf( + LinkTextState( + text = privacyPolicyText, + url = PLACEHOLDER_URL, + onClick = onPrivacyPolicyClick, + ), + LinkTextState( + text = termsOfUseText, + url = PLACEHOLDER_URL, + onClick = onTermsOfUseClick, + ), + ), + style = FirefoxTheme.typography.caption + .copy( + color = FirefoxTheme.colors.textSecondary, + ), + linkTextDecoration = TextDecoration.Underline, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Image( + painter = painterResource(id = R.drawable.shopping_onboarding), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(all = 16.dp), + ) + + Spacer(modifier = Modifier.height(12.dp)) PrimaryButton( text = stringResource(R.string.review_quality_check_contextual_onboarding_primary_button_text), @@ -78,46 +143,41 @@ fun ColumnScope.ReviewQualityCheckContextualOnboarding( Spacer(modifier = Modifier.height(8.dp)) TextButton( - text = stringResource(R.string.review_quality_check_contextual_onboarding_secondary_button_text), onClick = onSecondaryButtonClick, modifier = Modifier.fillMaxWidth(), - ) + ) { + Text( + text = stringResource(R.string.review_quality_check_contextual_onboarding_secondary_button_text), + color = FirefoxTheme.colors.textAccent, + style = FirefoxTheme.typography.button, + maxLines = 1, + ) + } } - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = stringResource( - R.string.review_quality_check_contextual_onboarding_caption, - stringResource(R.string.shopping_product_name), - stringResource(id = R.string.review_quality_check_contextual_onboarding_privacy_policy), - stringResource(id = R.string.review_quality_check_contextual_onboarding_terms_use), - ), - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.caption, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) } @Composable private fun createDescriptionString( - retailers: List = listOf( - R.string.review_quality_check_retailer_name_amazon, - R.string.review_quality_check_retailer_name_bestbuy, - R.string.review_quality_check_retailer_name_walmart, - ), + retailers: List, ) = buildAnnotatedString { + val retailerNames = retailers.map { + when (it) { + ProductVendor.AMAZON -> R.string.review_quality_check_retailer_name_amazon + ProductVendor.BEST_BUY -> R.string.review_quality_check_retailer_name_bestbuy + ProductVendor.WALMART -> R.string.review_quality_check_retailer_name_walmart + } + } + val description = stringResource( id = R.string.review_quality_check_contextual_onboarding_description, - stringResource(retailers[0]), + stringResource(retailerNames[0]), stringResource(R.string.app_name), - stringResource(retailers[1]), - stringResource(retailers[2]), + stringResource(retailerNames[1]), + stringResource(retailerNames[2]), ) append(description) - retailers.forEach { + retailerNames.forEach { val retailer = stringResource(id = it) val start = description.indexOf(retailer) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt index 9ed05c9fe..0d44b1855 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt @@ -110,7 +110,7 @@ fun ReviewQualityCheckInfoCard( LinkText( text = it.first, - linkTextState = listOf(it.second), + linkTextStates = listOf(it.second), style = FirefoxTheme.typography.body2.copy( color = FirefoxTheme.colors.textPrimary, ), diff --git a/app/src/main/res/drawable-hdpi/shopping_onboarding.webp b/app/src/main/res/drawable-hdpi/shopping_onboarding.webp new file mode 100644 index 000000000..f73288ac1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-mdpi/shopping_onboarding.webp b/app/src/main/res/drawable-mdpi/shopping_onboarding.webp new file mode 100644 index 000000000..925d64a93 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-night-hdpi/shopping_onboarding.webp b/app/src/main/res/drawable-night-hdpi/shopping_onboarding.webp new file mode 100644 index 000000000..99dfa5d60 Binary files /dev/null and b/app/src/main/res/drawable-night-hdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-night-mdpi/shopping_onboarding.webp b/app/src/main/res/drawable-night-mdpi/shopping_onboarding.webp new file mode 100644 index 000000000..9972cee8e Binary files /dev/null and b/app/src/main/res/drawable-night-mdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-night-xhdpi/shopping_onboarding.webp b/app/src/main/res/drawable-night-xhdpi/shopping_onboarding.webp new file mode 100644 index 000000000..cf9928436 Binary files /dev/null and b/app/src/main/res/drawable-night-xhdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-night-xxhdpi/shopping_onboarding.webp b/app/src/main/res/drawable-night-xxhdpi/shopping_onboarding.webp new file mode 100644 index 000000000..8a1606e65 Binary files /dev/null and b/app/src/main/res/drawable-night-xxhdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-night-xxxhdpi/shopping_onboarding.webp b/app/src/main/res/drawable-night-xxxhdpi/shopping_onboarding.webp new file mode 100644 index 000000000..93242d9fd Binary files /dev/null and b/app/src/main/res/drawable-night-xxxhdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-night/shopping_onboarding.webp b/app/src/main/res/drawable-night/shopping_onboarding.webp new file mode 100644 index 000000000..93242d9fd Binary files /dev/null and b/app/src/main/res/drawable-night/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-xhdpi/shopping_onboarding.webp b/app/src/main/res/drawable-xhdpi/shopping_onboarding.webp new file mode 100644 index 000000000..1d4ead34c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/shopping_onboarding.webp b/app/src/main/res/drawable-xxhdpi/shopping_onboarding.webp new file mode 100644 index 000000000..3d9cb1d18 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/shopping_onboarding.webp b/app/src/main/res/drawable-xxxhdpi/shopping_onboarding.webp new file mode 100644 index 000000000..791b0af3d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/shopping_onboarding.webp differ diff --git a/app/src/main/res/drawable/shopping_onboarding.webp b/app/src/main/res/drawable/shopping_onboarding.webp new file mode 100644 index 000000000..791b0af3d Binary files /dev/null and b/app/src/main/res/drawable/shopping_onboarding.webp differ 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 91637dd21..348ffe976 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 @@ -45,7 +45,7 @@ class ReviewQualityCheckStoreTest { dispatcher.scheduler.advanceUntilIdle() tested.waitUntilIdle() - val expected = ReviewQualityCheckState.NotOptedIn + val expected = ReviewQualityCheckState.NotOptedIn() assertEquals(expected, tested.state) } @@ -90,7 +90,7 @@ class ReviewQualityCheckStoreTest { tested.waitUntilIdle() dispatcher.scheduler.advanceUntilIdle() - val expected = ReviewQualityCheckState.NotOptedIn + val expected = ReviewQualityCheckState.NotOptedIn() assertEquals(expected, tested.state) }