tor-browser

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

commit 0416100db68ca75f883c93615e294587c25d0077
parent 8ae0ce3e2f6171b34375001f209d584dd563b41a
Author: Segun Famisa <sfamisa@mozilla.com>
Date:   Fri, 21 Nov 2025 14:33:27 +0000

Bug 1998194 - Add compose UI for credit card editor screen r=android-reviewers,android-l10n-reviewers,delphine,007

This patch introduces the Jetpack Compose UI for the "Add/Edit Credit Card" screen, which was previously a native Android view.

The following new files have been added:
- `CreditCardEditorScreen.kt`: The main composable for the screen, which handles both adding a new card and editing an existing one. It includes the top app bar, text input fields for card number and name, dropdowns for expiration date, and action buttons.
- `CreditCardEditorState.kt`: A new `State` data class that defines the model for the editor screen, holding properties like card details, expiry date options, and UI state flags (`inEditMode`, `showDeleteDialog`).
- `DeleteCreditCardDialog.kt`: A reusable composable for the confirmation dialog shown when deleting a credit card.

A new string resource (`credit_cards_navigate_back_button_content_description`) was also added for the back button's content description.

Differential Revision: https://phabricator.services.mozilla.com/D271087

Diffstat:
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorScreen.kt | 471+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorState.kt | 36++++++++++++++++++++++++++++++++++++
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorTestTags.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/DeleteCreditCardDialog.kt | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/res/values/strings.xml | 2++
5 files changed, 625 insertions(+), 0 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorScreen.kt @@ -0,0 +1,471 @@ +/* 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.settings.creditcards.ui + +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import mozilla.components.compose.base.Dropdown +import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview +import mozilla.components.compose.base.button.DestructiveButton +import mozilla.components.compose.base.button.FilledButton +import mozilla.components.compose.base.button.OutlinedButton +import mozilla.components.compose.base.menu.MenuItem +import mozilla.components.compose.base.text.Text +import mozilla.components.compose.base.textfield.TextField +import mozilla.components.compose.base.textfield.TextFieldColors +import mozilla.components.lib.state.ext.observeAsState +import org.mozilla.fenix.R +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorAction.DeleteDialogAction +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorAction.FieldChanged +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme +import mozilla.components.ui.icons.R as iconsR + +/** + * Weight for the expiration month dropdown. + */ +private const val EXPIRATION_MONTH_WEIGHT = 5f + +/** + * Weight for the expiration year dropdown. + */ +private const val EXPIRATION_YEAR_WEIGHT = 4f + +/** + * Composable for the credit card editor screen. + * + * @param store The [CreditCardEditorStore] that manages the state in the screen + */ +@Composable +fun CreditCardEditorScreen(store: CreditCardEditorStore) { + val state by store.observeAsState(store.state) { it } + + Scaffold( + modifier = Modifier.semantics { + testTagsAsResourceId = true + }, + topBar = { + EditorTopBar( + inEditMode = state.inEditMode, + onBackClicked = { + store.dispatch(CreditCardEditorAction.NavigateBack) + }, + onSaveActionClicked = { + store.dispatch(CreditCardEditorAction.Save) + }, + onDeleteActionClicked = { + store.dispatch(CreditCardEditorAction.DeleteClicked) + }, + ) + }, + containerColor = FirefoxTheme.colors.layer1, + ) { + if (state.showDeleteDialog) { + DeleteCreditCardDialog( + onCancel = { + store.dispatch(DeleteDialogAction.Cancel) + }, + onConfirm = { + store.dispatch(DeleteDialogAction.Confirm) + }, + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(it), + contentAlignment = Alignment.TopCenter, + ) { + EditorContent( + state = state, + onCardNumberChanged = { cardNumber -> + store.dispatch(FieldChanged.CardNumberChanged(cardNumber)) + }, + onNameOnCardChanged = { nameOnCard -> + store.dispatch(FieldChanged.NameOnCardChanged(nameOnCard)) + }, + onMonthSelected = { month -> + store.dispatch(FieldChanged.MonthSelected(month)) + }, + onYearSelected = { year -> + store.dispatch(FieldChanged.YearSelected(year)) + }, + onDeleteClicked = { + store.dispatch(CreditCardEditorAction.DeleteClicked) + }, + onCancelClicked = { + store.dispatch(CreditCardEditorAction.Cancel) + }, + onSaveClicked = { + store.dispatch(CreditCardEditorAction.Save) + }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) // TopAppBar +@Composable +private fun EditorTopBar( + modifier: Modifier = Modifier, + inEditMode: Boolean, + onDeleteActionClicked: () -> Unit, + onSaveActionClicked: () -> Unit, + onBackClicked: () -> Unit, +) { + TopAppBar( + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = FirefoxTheme.colors.layer1, + titleContentColor = FirefoxTheme.colors.textPrimary, + actionIconContentColor = FirefoxTheme.colors.iconPrimary, + ), + title = { + Text( + text = if (inEditMode) { + stringResource(R.string.credit_cards_edit_card) + } else { + stringResource(R.string.credit_cards_add_card) + }, + style = FirefoxTheme.typography.headline6, + ) + }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_back_24), + contentDescription = stringResource(R.string.credit_cards_navigate_back_button_content_description), + ) + } + }, + actions = { + if (inEditMode) { + IconButton( + modifier = Modifier.testTag(CreditCardEditorTestTags.TOPBAR_DELETE_BUTTON), + onClick = onDeleteActionClicked, + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_delete_24), + contentDescription = stringResource(R.string.credit_cards_menu_delete_card), + ) + } + } + IconButton( + modifier = Modifier.testTag(CreditCardEditorTestTags.TOPBAR_SAVE_BUTTON), + onClick = onSaveActionClicked, + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_checkmark_24), + contentDescription = stringResource(R.string.credit_cards_menu_save), + ) + } + }, + windowInsets = WindowInsets( + top = 0.dp, + bottom = 0.dp, + ), + ) +} + +@Composable +private fun EditorContent( + state: CreditCardEditorState, + onCardNumberChanged: (String) -> Unit = {}, + onNameOnCardChanged: (String) -> Unit = {}, + onMonthSelected: (Int) -> Unit = {}, + onYearSelected: (Int) -> Unit = {}, + onDeleteClicked: () -> Unit = {}, + onCancelClicked: () -> Unit = {}, + onSaveClicked: () -> Unit = {}, +) { + Column( + modifier = Modifier + .widthIn(max = FirefoxTheme.layout.size.containerMaxWidth) + .verticalScroll(rememberScrollState()) + .padding(FirefoxTheme.layout.space.static200) + .imePadding(), + verticalArrangement = Arrangement.spacedBy(FirefoxTheme.layout.space.static200), + ) { + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + TextInput( + modifier = Modifier + .fillMaxWidth() + .testTag(CreditCardEditorTestTags.CARD_NUMBER_FIELD) + .semantics { contentType = ContentType.CreditCardNumber } + .focusRequester(focusRequester), + errorText = stringResource(R.string.credit_cards_number_validation_error_message_2), + label = stringResource(R.string.credit_cards_card_number), + isError = state.showCardNumberError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next, + ), + value = state.cardNumber, + onValueChange = onCardNumberChanged, + ) + + TextInput( + modifier = Modifier + .fillMaxWidth() + .testTag(CreditCardEditorTestTags.NAME_ON_CARD_FIELD) + .semantics { contentType = ContentType.PersonFullName }, + errorText = stringResource(R.string.credit_cards_name_on_card_validation_error_message_2), + label = stringResource(R.string.credit_cards_name_on_card), + isError = state.showNameOnCardError, + value = state.nameOnCard, + onValueChange = onNameOnCardChanged, + ) + + ExpirationDateRow( + months = state.expiryMonths, + years = state.expiryYears, + selectedMonthIndex = state.selectedExpiryMonthIndex, + selectedYearIndex = state.selectedExpiryYearIndex, + onMonthSelected = onMonthSelected, + onYearSelected = onYearSelected, + ) + + ButtonsRow( + inEditMode = state.inEditMode, + onDeleteClicked = onDeleteClicked, + onCancelClicked = onCancelClicked, + onSaveClicked = onSaveClicked, + ) + } +} + +@Composable +private fun ExpirationDateRow( + months: List<String>, + selectedMonthIndex: Int, + years: List<String>, + selectedYearIndex: Int, + onMonthSelected: (Int) -> Unit, + onYearSelected: (Int) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(FirefoxTheme.layout.space.static200), + ) { + ExpirationDateDropdown( + modifier = Modifier + .weight(EXPIRATION_MONTH_WEIGHT) + .testTag(CreditCardEditorTestTags.EXPIRATION_MONTH_FIELD) + .semantics { contentType = ContentType.CreditCardExpirationMonth }, + label = stringResource(R.string.credit_cards_expiration_date_month), + items = months, + selectedIndex = selectedMonthIndex, + onItemSelected = onMonthSelected, + ) + + ExpirationDateDropdown( + modifier = Modifier + .weight(EXPIRATION_YEAR_WEIGHT) + .testTag(CreditCardEditorTestTags.EXPIRATION_YEAR_FIELD) + .semantics { contentType = ContentType.CreditCardExpirationYear }, + label = stringResource(R.string.credit_cards_expiration_date_year), + items = years, + selectedIndex = selectedYearIndex, + onItemSelected = onYearSelected, + ) + } +} + +@Composable +private fun ButtonsRow( + inEditMode: Boolean, + onDeleteClicked: () -> Unit, + onCancelClicked: () -> Unit, + onSaveClicked: () -> Unit, +) { + Row { + if (inEditMode) { + DestructiveButton( + text = stringResource(R.string.credit_cards_delete_card_button), + modifier = Modifier.testTag(CreditCardEditorTestTags.DELETE_BUTTON), + onClick = onDeleteClicked, + ) + } + + Spacer(Modifier.weight(1f)) + + OutlinedButton( + modifier = Modifier.testTag(CreditCardEditorTestTags.CANCEL_BUTTON), + text = stringResource(R.string.credit_cards_cancel_button), + onClick = onCancelClicked, + ) + + Spacer(Modifier.width(FirefoxTheme.layout.space.static200)) + + FilledButton( + modifier = Modifier.testTag(CreditCardEditorTestTags.SAVE_BUTTON), + text = stringResource(R.string.credit_cards_save_button), + onClick = onSaveClicked, + ) + } +} + +@Composable +private fun ExpirationDateDropdown( + label: String, + selectedIndex: Int, + items: List<String>, + modifier: Modifier = Modifier, + onItemSelected: (Int) -> Unit, +) { + Dropdown( + modifier = modifier, + label = label, + dropdownItems = items.mapIndexed { index, itemLabel -> + MenuItem.CheckableItem( + text = Text.String(value = itemLabel), + isChecked = index == selectedIndex, + onClick = { + onItemSelected(index) + }, + ) + }, + placeholder = "", + ) +} + +@Composable +private fun TextInput( + modifier: Modifier = Modifier, + label: String, + value: String, + isError: Boolean, + errorText: String, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + onValueChange: (String) -> Unit = {}, +) { + TextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = label, + placeholder = "", + isError = isError, + errorText = errorText, + keyboardOptions = keyboardOptions, + colors = TextFieldColors.default(), + ) +} + +@FlexibleWindowLightDarkPreview +@Composable +@Preview +private fun CreditCardEditorScreenPreview() = FirefoxTheme { + val state by remember { + mutableStateOf( + CreditCardEditorState( + guid = "1234", + cardNumber = "5555444433331111", + nameOnCard = "Jane Doe", + expiryMonths = listOf("January (01)", "February (02)"), + selectedExpiryMonthIndex = 1, + expiryYears = listOf("2025", "2026", "2027"), + selectedExpiryYearIndex = 2, + inEditMode = false, + showDeleteDialog = false, + ), + ) + } + CreditCardEditorScreen( + store = CreditCardEditorStore(initialState = state), + ) +} + +@Composable +@Preview +private fun CreditCardEditorScreenPrivateThemePreview() = FirefoxTheme(theme = Theme.Private) { + val state by remember { + mutableStateOf( + CreditCardEditorState( + guid = "1234", + cardNumber = "5555444433331111", + nameOnCard = "Jane Doe", + expiryMonths = listOf("January (01)", "February (02)"), + selectedExpiryMonthIndex = 1, + expiryYears = listOf("2025", "2026", "2027"), + selectedExpiryYearIndex = 2, + inEditMode = false, + showDeleteDialog = false, + ), + ) + } + CreditCardEditorScreen( + store = CreditCardEditorStore(initialState = state), + ) +} + +@FlexibleWindowLightDarkPreview +@Composable +@Preview +private fun CreditCardEditorScreenDeleteDialogPreview() = FirefoxTheme { + val state by remember { + mutableStateOf( + CreditCardEditorState( + guid = "1234", + cardNumber = "5555444433331111", + nameOnCard = "Jane Doe", + expiryMonths = listOf("January (01)", "February (02)"), + selectedExpiryMonthIndex = 1, + expiryYears = listOf("2025", "2026", "2027"), + selectedExpiryYearIndex = 2, + inEditMode = false, + showDeleteDialog = true, + ), + ) + } + CreditCardEditorScreen( + store = CreditCardEditorStore(initialState = state), + ) +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorState.kt @@ -0,0 +1,36 @@ +/* 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.settings.creditcards.ui + +import mozilla.components.lib.state.State + +/** + * State defining the "Edit Credit Card" screen + * + * @property guid The unique identifier for the edited credit card. + * @property cardNumber The credit card number of the card being edited. + * @property showCardNumberError Indicates whether or not to show an error on the "card number" field. + * @property nameOnCard The credit card name. + * @property showNameOnCardError Indicates whether or not to show an error on the "name on card" field. + * @property expiryMonths The non-empty list of expiry month options for display. + * @property selectedExpiryMonthIndex The index of the selected expiry month. + * @property expiryYears The non-empty list of expiry year options for display. + * @property selectedExpiryYearIndex The index of the selected expiry year. + * @property inEditMode Indicates whether or not the state is in edit more, or create mode. + * @property showDeleteDialog Indicates whether or not to show the delete dialog. + */ +data class CreditCardEditorState( + val guid: String, + val cardNumber: String, + val showCardNumberError: Boolean = false, + val nameOnCard: String, + val showNameOnCardError: Boolean = false, + val expiryMonths: List<String>, + val selectedExpiryMonthIndex: Int, + val expiryYears: List<String>, + val selectedExpiryYearIndex: Int, + val inEditMode: Boolean, + val showDeleteDialog: Boolean = false, +) : State diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorTestTags.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/CreditCardEditorTestTags.kt @@ -0,0 +1,44 @@ +/* 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.settings.creditcards.ui + +/** + * Test tags for the credit card editor screen. + */ +object CreditCardEditorTestTags { + + /** Test tag for the card number input field. */ + const val CARD_NUMBER_FIELD = "credit_card.edit.field.card_number" + + /** Test tag for the name on card input field. */ + const val NAME_ON_CARD_FIELD = "credit_card.edit.field.name_on_card" + + /** Test tag for the expiration month input field. */ + const val EXPIRATION_MONTH_FIELD = "credit_card.edit.field.expiration_month" + + /** Test tag for the expiration year input field. */ + const val EXPIRATION_YEAR_FIELD = "credit_card.edit.field.expiration_year" + + /** Test tag for the save button. */ + const val SAVE_BUTTON = "credit_card.edit.button.save" + + /** Test tag for the cancel button. */ + const val CANCEL_BUTTON = "credit_card.edit.button.cancel" + + /** Test tag for the delete button. */ + const val DELETE_BUTTON = "credit_card.edit.button.delete" + + /** Test tag for the cancel button in the delete confirmation dialog. */ + const val DELETE_DIALOG_CANCEL_BUTTON = "credit_card.edit.dialog.button.cancel" + + /** Test tag for the delete button in the delete confirmation dialog. */ + const val DELETE_DIALOG_DELETE_BUTTON = "credit_card.edit.dialog.button.delete" + + /** Test tag for the delete button in the top bar. */ + const val TOPBAR_DELETE_BUTTON = "credit_card.edit.topbar.button.delete" + + /** Test tag for the save button in the top bar. */ + const val TOPBAR_SAVE_BUTTON = "credit_card.edit.topbar.button.save" +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/DeleteCreditCardDialog.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/ui/DeleteCreditCardDialog.kt @@ -0,0 +1,72 @@ +/* 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.settings.creditcards.ui + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import mozilla.components.compose.base.button.TextButton +import org.mozilla.fenix.R +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +/** + * Composable for the delete credit card dialog. + * + * @param modifier The [Modifier] to be applied to the dialog. + * @param onCancel The callback to be invoked when the cancel button is clicked. + * @param onConfirm The callback to be invoked when the confirm button is clicked. + */ +@Composable +internal fun DeleteCreditCardDialog( + modifier: Modifier = Modifier, + onCancel: () -> Unit = {}, + onConfirm: () -> Unit = {}, +) { + AlertDialog( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.credit_cards_delete_dialog_confirmation_2), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline5, + ) + }, + text = null, + onDismissRequest = onCancel, + confirmButton = { + TextButton( + modifier = Modifier.testTag(CreditCardEditorTestTags.DELETE_DIALOG_DELETE_BUTTON), + text = stringResource(R.string.credit_cards_delete_dialog_button), + onClick = onConfirm, + ) + }, + dismissButton = { + TextButton( + modifier = Modifier.testTag(CreditCardEditorTestTags.DELETE_DIALOG_CANCEL_BUTTON), + text = stringResource(R.string.credit_cards_cancel_button), + onClick = onCancel, + ) + }, + ) +} + +private class ThemePreviewParameterProvider( + override val values: Sequence<Theme> = Theme.entries.asSequence(), +) : PreviewParameterProvider<Theme> + +@Composable +@Preview +private fun PreviewDeleteCreditCardDialog( + @PreviewParameter(ThemePreviewParameterProvider::class) theme: Theme, +) = FirefoxTheme(theme) { + DeleteCreditCardDialog() +} diff --git a/mobile/android/fenix/app/src/main/res/values/strings.xml b/mobile/android/fenix/app/src/main/res/values/strings.xml @@ -2529,6 +2529,8 @@ <!-- Title of the "Add card" screen --> <string name="credit_cards_add_card">Add card</string> + <!-- Content description for the "credit card feature" top bar back button --> + <string name="credit_cards_navigate_back_button_content_description">Navigate back</string> <!-- Title of the "Edit card" screen --> <string name="credit_cards_edit_card">Edit card</string> <!-- The header for the card number of a credit card -->