tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/addresses/FakeCreditCardsAddressesStorage.kt | 52++++++++++++++++++++++++++--------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/autofill/ui/AutofillSettingsScreen.kt | 347++++++++++++++++++++++++++++++-------------------------------------------------
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, + ) } }