commit 1333d0c8c3e65dd9d5a5e1586941f953137983d7
parent df3628f701876bdc60d6841fb829123a7cde0c0a
Author: Gabriel Luong <gabriel.luong@gmail.com>
Date: Tue, 2 Dec 2025 07:24:56 +0000
Bug 1993368 - Part 30: Migrate AutofillSettingsScreen to M3 specs r=android-reviewers,007
- Refactored to use the appropriate text and switch list items, and settings header.
- Added preview parameter to display the various states in the composable preview.
Figma: https://www.figma.com/design/ctk1Pw1TBxUwVgTTOvjHb4/2025-Android-Fundamentals?node-id=990-27973&m=dev
Differential Revision: https://phabricator.services.mozilla.com/D274533
Diffstat:
2 files changed, 159 insertions(+), 240 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/addresses/FakeCreditCardsAddressesStorage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/addresses/FakeCreditCardsAddressesStorage.kt
@@ -17,7 +17,7 @@ import java.util.UUID
/**
* Some randomly generated fake addresses that match the expected locale.
*/
-internal fun String.generateFakeAddressForLangTag(): UpdatableAddressFields = when (this) {
+fun String.generateFakeAddressForLangTag(): UpdatableAddressFields = when (this) {
"en-CA" -> UpdatableAddressFields(
name = "Tim Horton",
organization = "",
@@ -150,31 +150,6 @@ internal class FakeCreditCardsAddressesStorage : CreditCardsAddressesStorage {
throw UnsupportedOperationException()
}
- private fun UpdatableAddressFields.toAddress() =
- Address(
- guid = UUID.randomUUID().toString(),
- organization = organization,
- name = name,
- streetAddress = streetAddress,
- addressLevel1 = addressLevel1,
- addressLevel2 = addressLevel2,
- addressLevel3 = addressLevel3,
- postalCode = postalCode,
- country = country,
- tel = tel,
- email = email,
- )
-
- private fun NewCreditCardFields.toCreditCard() = CreditCard(
- guid = UUID.randomUUID().toString(),
- billingName = billingName,
- cardNumberLast4 = cardNumberLast4,
- expiryMonth = expiryMonth,
- expiryYear = expiryYear,
- cardType = cardType,
- encryptedCardNumber = CreditCardNumber.Encrypted(plaintextCardNumber.number),
- )
-
companion object {
fun getAllPossibleLocaleLangTags(): List<String> = listOf(
"US",
@@ -200,5 +175,30 @@ internal class FakeCreditCardsAddressesStorage : CreditCardsAddressesStorage {
cardType = randomCardTypes.random(),
)
}
+
+ fun NewCreditCardFields.toCreditCard() = CreditCard(
+ guid = UUID.randomUUID().toString(),
+ billingName = billingName,
+ cardNumberLast4 = cardNumberLast4,
+ expiryMonth = expiryMonth,
+ expiryYear = expiryYear,
+ cardType = cardType,
+ encryptedCardNumber = CreditCardNumber.Encrypted(plaintextCardNumber.number),
+ )
+
+ fun UpdatableAddressFields.toAddress() =
+ Address(
+ guid = UUID.randomUUID().toString(),
+ organization = organization,
+ name = name,
+ streetAddress = streetAddress,
+ addressLevel1 = addressLevel1,
+ addressLevel2 = addressLevel2,
+ addressLevel3 = addressLevel3,
+ postalCode = postalCode,
+ country = country,
+ tel = tel,
+ email = email,
+ )
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/autofill/ui/AutofillSettingsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/autofill/ui/AutofillSettingsScreen.kt
@@ -4,52 +4,49 @@
package org.mozilla.fenix.settings.autofill.ui
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-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.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
import mozilla.components.compose.base.button.IconButton
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCard
import mozilla.components.lib.state.ext.observeAsState
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.list.IconListItem
+import org.mozilla.fenix.compose.list.SwitchListItem
+import org.mozilla.fenix.compose.list.TextListItem
+import org.mozilla.fenix.compose.settings.SettingsSectionHeader
+import org.mozilla.fenix.debugsettings.addresses.FakeCreditCardsAddressesStorage.Companion.generateCreditCard
+import org.mozilla.fenix.debugsettings.addresses.FakeCreditCardsAddressesStorage.Companion.toAddress
+import org.mozilla.fenix.debugsettings.addresses.FakeCreditCardsAddressesStorage.Companion.toCreditCard
+import org.mozilla.fenix.debugsettings.addresses.generateFakeAddressForLangTag
import org.mozilla.fenix.theme.FirefoxTheme
+import org.mozilla.fenix.theme.Theme
import mozilla.components.ui.icons.R as iconsR
-private const val WEIGHT_TEXT = 5f
-private const val WEIGHT_SWITCH = 2f
-
@Composable
internal fun AutofillSettingsScreen(
buildStore: (NavHostController) -> AutofillSettingsStore,
@@ -84,26 +81,21 @@ internal fun AutofillSettingsScreen(
topBar = {
AutofillSettingsTopBar(store)
},
- containerColor = FirefoxTheme.colors.layer1,
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxWidth(),
) {
- Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static200))
+ Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static100))
+
AutofillSettingsAddressSection(store, isAddressSyncEnabled)
- // delimiter line between sections
- HorizontalDivider(
- modifier = Modifier.padding(
- start = 4.dp,
- end = 4.dp,
- top = 16.dp,
- bottom = 16.dp,
- ),
- color = FirefoxTheme.colors.indicatorInactive, thickness = 0.3.dp,
- )
+ Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static200))
+
+ HorizontalDivider()
+
+ Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static300))
AutofillSettingsCreditCardSection(store)
}
@@ -114,7 +106,6 @@ internal fun AutofillSettingsScreen(
@Composable
private fun AutofillSettingsTopBar(store: AutofillSettingsStore) {
TopAppBar(
- colors = TopAppBarDefaults.topAppBarColors(containerColor = FirefoxTheme.colors.layer1),
windowInsets = WindowInsets(
top = 0.dp,
bottom = 0.dp,
@@ -122,14 +113,11 @@ private fun AutofillSettingsTopBar(store: AutofillSettingsStore) {
title = {
Text(
text = stringResource(R.string.preferences_autofill),
- color = FirefoxTheme.colors.textPrimary,
- style = FirefoxTheme.typography.headline6,
+ style = FirefoxTheme.typography.headline5,
)
},
navigationIcon = {
IconButton(
- modifier = Modifier
- .padding(horizontal = FirefoxTheme.layout.space.static50),
onClick = { store.dispatch(AutofillSettingsBackClicked) },
contentDescription = stringResource(
R.string.autofill_settings_navigate_back_button_content_description,
@@ -138,7 +126,6 @@ private fun AutofillSettingsTopBar(store: AutofillSettingsStore) {
Icon(
painter = painterResource(iconsR.drawable.mozac_ic_back_24),
contentDescription = null,
- tint = FirefoxTheme.colors.iconPrimary,
)
}
},
@@ -152,44 +139,52 @@ private fun AutofillSettingsAddressSection(
) {
val state by store.observeAsState(store.state) { it }
- AddSectionLabel(label = stringResource(id = R.string.preferences_addresses))
+ SettingsSectionHeader(
+ text = stringResource(id = R.string.preferences_addresses),
+ modifier = Modifier.padding(horizontal = FirefoxTheme.layout.space.dynamic200),
+ )
+
Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static200))
- SaveFillSwitch(
- title = stringResource(id = R.string.preferences_addresses_save_and_autofill_addresses_2),
- label = stringResource(id = R.string.preferences_addresses_save_and_autofill_addresses_summary_2),
- isChecked = state.saveFillAddresses,
- onStateChange = { store.dispatch(ChangeAddressSaveFillPreference(!state.saveFillAddresses)) },
+
+ SwitchListItem(
+ label = stringResource(id = R.string.preferences_addresses_save_and_autofill_addresses_2),
+ checked = state.saveFillAddresses,
+ description = stringResource(id = R.string.preferences_addresses_save_and_autofill_addresses_summary_2),
+ showSwitchAfter = true,
+ onClick = { store.dispatch(ChangeAddressSaveFillPreference(!state.saveFillAddresses)) },
)
- Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static100))
+
+ Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static200))
if (isAddressSyncEnabled) {
if (state.accountAuthState == AccountAuthState.Authenticated) {
- SyncAcrossDevicesForAuthenticatedAccount(
- text = stringResource(id = R.string.preferences_addresses_sync_addresses),
- isChecked = state.syncAddresses,
- onSyncStateChange = { store.dispatch(UpdateAddressesSyncStatus(!state.syncAddresses)) },
- )
+ SwitchListItem(
+ label = stringResource(id = R.string.preferences_addresses_sync_addresses),
+ checked = state.syncAddresses,
+ showSwitchAfter = true,
+ ) {
+ store.dispatch(UpdateAddressesSyncStatus(!state.syncAddresses))
+ }
} else {
- SyncAcrossDevicesForNotAuthenticatedAccount(
- text = stringResource(id = R.string.preferences_addresses_sync_addresses_across_devices),
+ TextListItem(
+ label = stringResource(id = R.string.preferences_addresses_sync_addresses_across_devices),
onClick = { store.dispatch(SyncAddressesAcrossDevicesClicked) },
)
}
}
- if (state.addresses.isEmpty()) {
- Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static100))
+ Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static200))
- AddItem(
+ if (state.addresses.isEmpty()) {
+ IconListItem(
label = stringResource(R.string.preferences_addresses_add_address),
- onAddItemClicked = { store.dispatch(AddAddressClicked) },
+ beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_plus_24),
+ onClick = { store.dispatch(AddAddressClicked) },
)
} else {
- Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static150))
-
- ManageItem(
- text = stringResource(id = R.string.preferences_addresses_manage_addresses),
- onClick = { store.dispatch(ManageAddressesClicked) },
+ TextListItem(
+ label = stringResource(id = R.string.preferences_addresses_manage_addresses),
+ onClick = { store.dispatch(AddAddressClicked) },
)
}
}
@@ -198,198 +193,122 @@ private fun AutofillSettingsAddressSection(
private fun AutofillSettingsCreditCardSection(store: AutofillSettingsStore) {
val state by store.observeAsState(store.state) { it }
- AddSectionLabel(label = stringResource(id = R.string.preferences_credit_cards_2))
+ SettingsSectionHeader(
+ text = stringResource(id = R.string.preferences_credit_cards_2),
+ modifier = Modifier.padding(horizontal = FirefoxTheme.layout.space.dynamic200),
+ )
+
Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static200))
- SaveFillSwitch(
- title = stringResource(id = R.string.preferences_credit_cards_save_and_autofill_cards_2),
- label = stringResource(
+
+ SwitchListItem(
+ label = stringResource(id = R.string.preferences_credit_cards_save_and_autofill_cards_2),
+ checked = state.saveFillCards,
+ description = stringResource(
id = R.string.preferences_credit_cards_save_and_autofill_cards_summary_2,
stringResource(id = R.string.app_name),
),
- isChecked = state.saveFillCards,
- onStateChange = { store.dispatch(ChangeCardSaveFillPreference(!state.saveFillCards)) },
- )
+ showSwitchAfter = true,
+ ) {
+ store.dispatch(ChangeCardSaveFillPreference(!state.saveFillCards))
+ }
+
Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static100))
if (state.accountAuthState == AccountAuthState.Authenticated) {
- SyncAcrossDevicesForAuthenticatedAccount(
- text = stringResource(id = R.string.preferences_credit_cards_sync_cards),
- isChecked = state.syncCreditCards,
- onSyncStateChange = { store.dispatch(UpdateCreditCardsSyncStatus(!state.syncCreditCards)) },
- )
+ SwitchListItem(
+ label = stringResource(id = R.string.preferences_credit_cards_sync_cards),
+ checked = state.syncCreditCards,
+ showSwitchAfter = true,
+ ) {
+ store.dispatch(UpdateCreditCardsSyncStatus(!state.syncCreditCards))
+ }
} else {
- SyncAcrossDevicesForNotAuthenticatedAccount(
- text = stringResource(id = R.string.preferences_credit_cards_sync_cards_across_devices),
+ TextListItem(
+ label = stringResource(id = R.string.preferences_credit_cards_sync_cards_across_devices),
onClick = { store.dispatch(SyncCardsAcrossDevicesClicked) },
)
}
- if (state.creditCards.isEmpty()) {
- Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static100))
+ Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static100))
- AddItem(
+ if (state.creditCards.isEmpty()) {
+ IconListItem(
label = stringResource(R.string.credit_cards_add_card),
- onAddItemClicked = { store.dispatch(AddCardClicked) },
+ beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_plus_24),
+ onClick = { store.dispatch(AddCardClicked) },
)
} else {
- Spacer(modifier = Modifier.height(FirefoxTheme.layout.space.static150))
- ManageItem(
- text = stringResource(id = R.string.preferences_credit_cards_manage_saved_cards_2),
- onClick = { store.dispatch(ManageCreditCardsClicked) },
- )
+ TextListItem(
+ label = stringResource(id = R.string.preferences_credit_cards_manage_saved_cards_2),
+ ) {
+ store.dispatch(ManageCreditCardsClicked)
+ }
}
}
-@Composable
-private fun AddSectionLabel(label: String) {
- Text(
- text = label,
- modifier = Modifier.padding(start = FirefoxTheme.layout.space.static200),
- style = FirefoxTheme.typography.headline8,
- color = MaterialTheme.colorScheme.tertiary,
+private data class AutofillSettingsScreenPreviewState(
+ val addresses: List<Address> = emptyList(),
+ val creditCards: List<CreditCard> = emptyList(),
+ val accountAuthState: AccountAuthState = AccountAuthState.LoggedOut,
+)
+
+private class AutofillSettingsScreenPreviewProvider : PreviewParameterProvider<AutofillSettingsScreenPreviewState> {
+ override val values = sequenceOf(
+ AutofillSettingsScreenPreviewState(),
+ AutofillSettingsScreenPreviewState(
+ addresses = listOf(
+ "en-CA".generateFakeAddressForLangTag().toAddress(),
+ ),
+ creditCards = listOf(generateCreditCard().toCreditCard()),
+ ),
+ AutofillSettingsScreenPreviewState(
+ accountAuthState = AccountAuthState.Authenticated,
+ ),
)
}
@Composable
-private fun SaveFillSwitch(
- title: String,
- label: String,
- isChecked: Boolean,
- onStateChange: (Boolean) -> Unit,
+@FlexibleWindowLightDarkPreview
+private fun AutofillSettingsScreenPreview(
+ @PreviewParameter(AutofillSettingsScreenPreviewProvider::class) param: AutofillSettingsScreenPreviewState,
) {
- val interactionSource = remember { MutableInteractionSource() }
- Text(
- text = title,
- modifier = Modifier.padding(start = FirefoxTheme.layout.space.static400),
- style = FirefoxTheme.typography.body1,
- color = FirefoxTheme.colors.textPrimary,
- )
-
- Row(
- modifier = Modifier
- .clickable(
- interactionSource = interactionSource,
- indication = null,
- role = Role.Switch,
- onClick = {
- onStateChange(!isChecked)
- },
- )
- .width(FirefoxTheme.layout.size.containerMaxWidth),
- horizontalArrangement = Arrangement.Absolute.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = label,
- modifier = Modifier
- .padding(start = FirefoxTheme.layout.space.static500)
- .weight(WEIGHT_TEXT),
- maxLines = 2,
- style = FirefoxTheme.typography.body2,
- color = FirefoxTheme.colors.textSecondary,
- )
-
- Switch(
- checked = isChecked,
- modifier = Modifier.weight(WEIGHT_SWITCH),
- onCheckedChange = {
- onStateChange(it)
- },
+ val store = { _: NavHostController ->
+ AutofillSettingsStore(
+ initialState = AutofillSettingsState.default.copy(
+ addresses = param.addresses,
+ creditCards = param.creditCards,
+ accountAuthState = param.accountAuthState,
+ ),
)
}
-}
-
-@Composable
-private fun SyncAcrossDevicesForAuthenticatedAccount(
- text: String,
- isChecked: Boolean,
- onSyncStateChange: (Boolean) -> Unit,
-) {
- val interactionSource = remember { MutableInteractionSource() }
- Row(
- modifier = Modifier
- .clickable(
- interactionSource = interactionSource,
- indication = null,
- role = Role.Switch,
- onClick = {
- onSyncStateChange(!isChecked)
- },
- )
- .width(FirefoxTheme.layout.size.containerMaxWidth),
- horizontalArrangement = Arrangement.Absolute.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = text,
- modifier = Modifier
- .padding(start = FirefoxTheme.layout.space.static400)
- .weight(WEIGHT_TEXT),
- maxLines = 2,
- style = FirefoxTheme.typography.body1,
- color = FirefoxTheme.colors.textPrimary,
- )
-
- Switch(
- checked = isChecked,
- modifier = Modifier.weight(WEIGHT_SWITCH),
- onCheckedChange = {
- onSyncStateChange(it)
- },
+ FirefoxTheme {
+ AutofillSettingsScreen(
+ buildStore = store,
+ accountManager = null,
+ isAddressSyncEnabled = true,
)
}
}
@Composable
-private fun SyncAcrossDevicesForNotAuthenticatedAccount(text: String, onClick: () -> Unit) {
- Text(
- text = text,
- modifier = Modifier
- .padding(start = FirefoxTheme.layout.space.static400)
- .clickable(onClick = onClick),
- style = FirefoxTheme.typography.body1,
- color = FirefoxTheme.colors.textPrimary,
- )
-}
-
-@Composable
-private fun AddItem(
- label: String,
- onAddItemClicked: () -> Unit,
+@Preview
+private fun AutofillSettingsScreenPrivatePreview(
+ @PreviewParameter(AutofillSettingsScreenPreviewProvider::class) param: AutofillSettingsScreenPreviewState,
) {
- IconListItem(
- label = label,
- modifier = Modifier
- .padding(start = 10.dp)
- .width(FirefoxTheme.layout.size.containerMaxWidth),
- beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_plus_24),
- onClick = { onAddItemClicked() },
- )
-}
-
-@Composable
-private fun ManageItem(text: String, onClick: () -> Unit) {
- Text(
- text = text,
- modifier = Modifier
- .padding(start = FirefoxTheme.layout.space.static400)
- .clickable(onClick = onClick),
- style = FirefoxTheme.typography.body1,
- color = FirefoxTheme.colors.textPrimary,
- )
-}
-
-@Composable
-@FlexibleWindowLightDarkPreview
-private fun AutofillSettingsScreenPreview() {
val store = { _: NavHostController ->
AutofillSettingsStore(
- initialState = AutofillSettingsState.default,
+ initialState = AutofillSettingsState.default.copy(
+ addresses = param.addresses,
+ creditCards = param.creditCards,
+ accountAuthState = param.accountAuthState,
+ ),
)
}
- FirefoxTheme {
- Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
- AutofillSettingsScreen(store, null, true)
- }
+ FirefoxTheme(theme = Theme.Private) {
+ AutofillSettingsScreen(
+ buildStore = store,
+ accountManager = null,
+ isAddressSyncEnabled = true,
+ )
}
}