Bug 1895440: add new UX for strong password generator feature r=boek,android-reviewers,delphine,geckoview-reviewers,007

Differential Revision: https://phabricator.services.mozilla.com/D211003
This commit is contained in:
alexandra.virvara 2024-06-05 14:02:50 +00:00
parent f114f4f423
commit a6c628a00d
18 changed files with 763 additions and 227 deletions

View File

@ -191,7 +191,7 @@ sealed class PromptRequest(
* Value type that represents a request for a select login prompt. * Value type that represents a request for a select login prompt.
* @property logins a list of logins that are associated with the current domain. * @property logins a list of logins that are associated with the current domain.
* @property generatedPassword the suggested strong password that was generated. * @property generatedPassword the suggested strong password that was generated.
* @property onConfirm callback that is called when the user wants to save the login. * @property onConfirm callback that is called when the user wants to select the login.
* @property onDismiss callback to let the page know the user dismissed the dialog. * @property onDismiss callback to let the page know the user dismissed the dialog.
*/ */
data class SelectLoginPrompt( data class SelectLoginPrompt(

View File

@ -46,6 +46,7 @@ import mozilla.components.concept.identitycredential.Account
import mozilla.components.concept.identitycredential.Provider import mozilla.components.concept.identitycredential.Provider
import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.CreditCardValidationDelegate import mozilla.components.concept.storage.CreditCardValidationDelegate
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry import mozilla.components.concept.storage.LoginEntry
import mozilla.components.concept.storage.LoginValidationDelegate import mozilla.components.concept.storage.LoginValidationDelegate
import mozilla.components.feature.prompts.address.AddressDelegate import mozilla.components.feature.prompts.address.AddressDelegate
@ -86,6 +87,9 @@ import mozilla.components.feature.prompts.identitycredential.SelectProviderDialo
import mozilla.components.feature.prompts.login.LoginDelegate import mozilla.components.feature.prompts.login.LoginDelegate
import mozilla.components.feature.prompts.login.LoginExceptions import mozilla.components.feature.prompts.login.LoginExceptions
import mozilla.components.feature.prompts.login.LoginPicker import mozilla.components.feature.prompts.login.LoginPicker
import mozilla.components.feature.prompts.login.PasswordGeneratorDialogColors
import mozilla.components.feature.prompts.login.PasswordGeneratorDialogColorsProvider
import mozilla.components.feature.prompts.login.PasswordGeneratorDialogFragment
import mozilla.components.feature.prompts.login.StrongPasswordPromptViewListener import mozilla.components.feature.prompts.login.StrongPasswordPromptViewListener
import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate
import mozilla.components.feature.prompts.share.DefaultShareDelegate import mozilla.components.feature.prompts.share.DefaultShareDelegate
@ -154,6 +158,10 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* feature is enabled or not. If this resolves to 'false', the feature will be hidden. * feature is enabled or not. If this resolves to 'false', the feature will be hidden.
* @property onSaveLoginWithStrongPassword A callback invoked to save a new login that uses the * @property onSaveLoginWithStrongPassword A callback invoked to save a new login that uses the
* generated strong password * generated strong password
* @property shouldAutomaticallyShowSuggestedPassword A callback invoked to check whether the user
* is engaging with signup for the first time.
* @property onFirstTimeEngagedWithSignup A callback invoked when user is engaged with signup for
* the first time.
* @property creditCardDelegate Delegate for credit card picker. * @property creditCardDelegate Delegate for credit card picker.
* @property addressDelegate Delegate for address picker. * @property addressDelegate Delegate for address picker.
* @property fileUploadsDirCleaner a [FileUploadsDirCleaner] to clean up temporary file uploads. * @property fileUploadsDirCleaner a [FileUploadsDirCleaner] to clean up temporary file uploads.
@ -161,7 +169,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* need to be requested before a prompt (e.g. a file picker) can be displayed. * need to be requested before a prompt (e.g. a file picker) can be displayed.
* Once the request is completed, [onPermissionsResult] needs to be invoked. * Once the request is completed, [onPermissionsResult] needs to be invoked.
*/ */
@Suppress("LargeClass", "LongParameterList") @Suppress("LargeClass", "LongParameterList", "MaxLineLength")
class PromptFeature private constructor( class PromptFeature private constructor(
private val container: PromptContainer, private val container: PromptContainer,
private val store: BrowserStore, private val store: BrowserStore,
@ -184,7 +192,13 @@ class PromptFeature private constructor(
private val suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object : private val suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
SuggestStrongPasswordDelegate {}, SuggestStrongPasswordDelegate {},
private val isSuggestStrongPasswordEnabled: Boolean = false, private val isSuggestStrongPasswordEnabled: Boolean = false,
private var shouldAutomaticallyShowSuggestedPassword: () -> Boolean = { false },
private val onFirstTimeEngagedWithSignup: () -> Unit = {},
private val onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> }, private val onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
private val onSavedGeneratedPassword: () -> Unit = {},
private val passwordGeneratorColorsProvider: PasswordGeneratorDialogColorsProvider = PasswordGeneratorDialogColorsProvider {
PasswordGeneratorDialogColors.default()
},
private val creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {}, private val creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
private val addressDelegate: AddressDelegate = DefaultAddressDelegate(), private val addressDelegate: AddressDelegate = DefaultAddressDelegate(),
private val fileUploadsDirCleaner: FileUploadsDirCleaner, private val fileUploadsDirCleaner: FileUploadsDirCleaner,
@ -233,7 +247,13 @@ class PromptFeature private constructor(
suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object : suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
SuggestStrongPasswordDelegate {}, SuggestStrongPasswordDelegate {},
isSuggestStrongPasswordEnabled: Boolean = false, isSuggestStrongPasswordEnabled: Boolean = false,
shouldAutomaticallyShowSuggestedPassword: () -> Boolean = { false },
onFirstTimeEngagedWithSignup: () -> Unit = {},
onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> }, onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
onSavedGeneratedPassword: () -> Unit = {},
passwordGeneratorColorsProvider: PasswordGeneratorDialogColorsProvider = PasswordGeneratorDialogColorsProvider {
PasswordGeneratorDialogColors.default()
},
creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {}, creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
addressDelegate: AddressDelegate = DefaultAddressDelegate(), addressDelegate: AddressDelegate = DefaultAddressDelegate(),
fileUploadsDirCleaner: FileUploadsDirCleaner, fileUploadsDirCleaner: FileUploadsDirCleaner,
@ -259,7 +279,11 @@ class PromptFeature private constructor(
loginDelegate = loginDelegate, loginDelegate = loginDelegate,
suggestStrongPasswordDelegate = suggestStrongPasswordDelegate, suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled, isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
shouldAutomaticallyShowSuggestedPassword = shouldAutomaticallyShowSuggestedPassword,
onFirstTimeEngagedWithSignup = onFirstTimeEngagedWithSignup,
onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword, onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
onSavedGeneratedPassword = onSavedGeneratedPassword,
passwordGeneratorColorsProvider = passwordGeneratorColorsProvider,
creditCardDelegate = creditCardDelegate, creditCardDelegate = creditCardDelegate,
addressDelegate = addressDelegate, addressDelegate = addressDelegate,
) )
@ -283,7 +307,10 @@ class PromptFeature private constructor(
suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object : suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
SuggestStrongPasswordDelegate {}, SuggestStrongPasswordDelegate {},
isSuggestStrongPasswordEnabled: Boolean = false, isSuggestStrongPasswordEnabled: Boolean = false,
shouldAutomaticallyShowSuggestedPassword: () -> Boolean = { false },
onFirstTimeEngagedWithSignup: () -> Unit = {},
onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> }, onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
onSavedGeneratedPassword: () -> Unit = {},
creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {}, creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
addressDelegate: AddressDelegate = DefaultAddressDelegate(), addressDelegate: AddressDelegate = DefaultAddressDelegate(),
fileUploadsDirCleaner: FileUploadsDirCleaner, fileUploadsDirCleaner: FileUploadsDirCleaner,
@ -308,7 +335,10 @@ class PromptFeature private constructor(
loginDelegate = loginDelegate, loginDelegate = loginDelegate,
suggestStrongPasswordDelegate = suggestStrongPasswordDelegate, suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled, isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
shouldAutomaticallyShowSuggestedPassword = shouldAutomaticallyShowSuggestedPassword,
onFirstTimeEngagedWithSignup = onFirstTimeEngagedWithSignup,
onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword, onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
onSavedGeneratedPassword = onSavedGeneratedPassword,
creditCardDelegate = creditCardDelegate, creditCardDelegate = creditCardDelegate,
addressDelegate = addressDelegate, addressDelegate = addressDelegate,
) )
@ -572,14 +602,20 @@ class PromptFeature private constructor(
return return
} }
if (promptRequest.generatedPassword != null && isSuggestStrongPasswordEnabled) { if (promptRequest.generatedPassword != null && isSuggestStrongPasswordEnabled) {
val currentUrl = if (shouldAutomaticallyShowSuggestedPassword.invoke()) {
store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.content?.url onFirstTimeEngagedWithSignup.invoke()
if (currentUrl != null) { handleDialogsRequest(
strongPasswordPromptViewListener?.handleSuggestStrongPasswordRequest(
promptRequest, promptRequest,
currentUrl, session,
onSaveLoginWithStrongPassword,
) )
} else {
strongPasswordPromptViewListener?.onGeneratedPasswordPromptClick = {
handleDialogsRequest(
promptRequest,
session,
)
}
strongPasswordPromptViewListener?.handleSuggestStrongPasswordRequest()
} }
} else { } else {
loginPicker?.handleSelectLoginRequest(promptRequest) loginPicker?.handleSelectLoginRequest(promptRequest)
@ -694,6 +730,7 @@ class PromptFeature private constructor(
is PromptRequest.IdentityCredential.SelectProvider -> it.onConfirm(value as Provider) is PromptRequest.IdentityCredential.SelectProvider -> it.onConfirm(value as Provider)
is PromptRequest.IdentityCredential.SelectAccount -> it.onConfirm(value as Account) is PromptRequest.IdentityCredential.SelectAccount -> it.onConfirm(value as Account)
is PromptRequest.IdentityCredential.PrivacyPolicy -> it.onConfirm(value as Boolean) is PromptRequest.IdentityCredential.PrivacyPolicy -> it.onConfirm(value as Boolean)
is SelectLoginPrompt -> it.onConfirm(value as Login)
else -> { else -> {
// no-op // no-op
} }
@ -760,6 +797,29 @@ class PromptFeature private constructor(
) { ) {
// Requests that are handled with dialogs // Requests that are handled with dialogs
val dialog = when (promptRequest) { val dialog = when (promptRequest) {
is SelectLoginPrompt -> {
val currentUrl =
store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.content?.url
val generatedPassword = promptRequest.generatedPassword
if (generatedPassword == null || currentUrl == null) {
logger.debug(
"Ignoring received SelectLogin.onGeneratedPasswordPromptClick" +
" when either the generated password or the currentUrl is null.",
)
dismissDialogRequest(promptRequest, session)
return
}
PasswordGeneratorDialogFragment.newInstance(
sessionId = session.id,
promptRequestUID = promptRequest.uid,
generatedPassword = generatedPassword,
currentUrl = currentUrl,
onSavedGeneratedPassword = onSavedGeneratedPassword,
colorsProvider = passwordGeneratorColorsProvider,
)
}
is SaveCreditCard -> { is SaveCreditCard -> {
if (!isCreditCardAutofillEnabled.invoke() || creditCardValidationDelegate == null || if (!isCreditCardAutofillEnabled.invoke() || creditCardValidationDelegate == null ||
!promptRequest.creditCard.isValid !promptRequest.creditCard.isValid

View File

@ -12,13 +12,9 @@ interface PasswordPromptView {
var listener: Listener? var listener: Listener?
/** /**
* Shows a simple prompt with the given [generatedPassword]. * Shows a simple prompt for using a generated password.
*/ */
fun showPrompt( fun showPrompt()
generatedPassword: String,
url: String,
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
)
/** /**
* Hides the prompt. * Hides the prompt.
@ -30,13 +26,8 @@ interface PasswordPromptView {
*/ */
interface Listener { interface Listener {
/** /**
* Called when a user wants to use a strong generated password. * Called when a user clicks on the password generator prompt
*
*/ */
fun onUseGeneratedPassword( fun onGeneratedPasswordPromptClick()
generatedPassword: String,
url: String,
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
)
} }
} }

View File

@ -0,0 +1,212 @@
/* 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 mozilla.components.feature.prompts.login
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
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.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.identitycredential.previews.DialogPreviewMaterialTheme
private val FONT_SIZE = 16.sp
private val LINE_HEIGHT = 24.sp
private val LETTER_SPACING = 0.15.sp
/**
* The password generator bottom sheet
*
* @param generatedStrongPassword The generated password.
* @param onUsePassword Invoked when the user clicks on the UsePassword button.
* @param onCancelDialog Invoked when the user clicks on the NotNow button.
* @param colors The colors of the dialog.
*/
@Composable
fun PasswordGeneratorBottomSheet(
generatedStrongPassword: String,
onUsePassword: () -> Unit,
onCancelDialog: () -> Unit,
colors: PasswordGeneratorDialogColors = PasswordGeneratorDialogColors.default(),
) {
Column(
modifier = Modifier
.background(colors.background)
.padding(all = 8.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
StrongPasswordBottomSheetTitle(colors = colors)
StrongPasswordBottomSheetDescription(colors = colors)
StrongPasswordBottomSheetPasswordBox(
generatedPassword = generatedStrongPassword,
colors = colors,
)
StrongPasswordBottomSheetButtons(
onUsePassword = { onUsePassword() },
onCancelDialog = { onCancelDialog() },
colors = colors,
)
}
}
@Composable
private fun StrongPasswordBottomSheetTitle(colors: PasswordGeneratorDialogColors) {
Row {
Image(
painter = painterResource(id = R.drawable.mozac_ic_login_24),
contentDescription = null,
contentScale = ContentScale.FillWidth,
colorFilter = ColorFilter.tint(colors.title),
modifier = Modifier
.align(Alignment.CenterVertically),
)
Text(
modifier = Modifier.padding(16.dp),
text = stringResource(id = R.string.mozac_feature_prompts_suggest_strong_password_title),
style = TextStyle(
fontSize = FONT_SIZE,
lineHeight = LINE_HEIGHT,
color = colors.title,
letterSpacing = LETTER_SPACING,
fontWeight = FontWeight.Bold,
),
)
}
}
@Composable
private fun StrongPasswordBottomSheetDescription(
modifier: Modifier = Modifier,
colors: PasswordGeneratorDialogColors,
) {
Text(
modifier = modifier.padding(start = 40.dp, top = 0.dp, end = 12.dp, bottom = 16.dp),
text = stringResource(id = R.string.mozac_feature_prompts_suggest_strong_password_description),
style = TextStyle(
fontSize = FONT_SIZE,
lineHeight = LINE_HEIGHT,
color = colors.description,
letterSpacing = LETTER_SPACING,
),
)
}
@Composable
private fun StrongPasswordBottomSheetPasswordBox(
modifier: Modifier = Modifier,
generatedPassword: String,
colors: PasswordGeneratorDialogColors,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 40.dp, top = 8.dp, end = 12.dp, bottom = 8.dp)
.background(colors.passwordBox)
.border(1.dp, colors.boxBorder)
.padding(4.dp),
) {
Text(
modifier = modifier.padding(8.dp),
text = generatedPassword,
style = TextStyle(
fontSize = FONT_SIZE,
lineHeight = LINE_HEIGHT,
color = colors.title,
letterSpacing = LETTER_SPACING,
),
)
}
}
@Composable
private fun StrongPasswordBottomSheetButtons(
onUsePassword: () -> Unit,
onCancelDialog: () -> Unit,
colors: PasswordGeneratorDialogColors,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.End),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp)
.height(48.dp),
) {
TextButton(
onClick = { onCancelDialog() },
shape = RectangleShape,
colors = ButtonDefaults.buttonColors(backgroundColor = colors.background),
modifier = Modifier.height(48.dp),
) {
Text(
text = stringResource(id = R.string.mozac_feature_prompt_not_now),
style = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
color = colors.confirmButton,
letterSpacing = 0.15.sp,
fontWeight = FontWeight.Bold,
),
)
}
Button(
onClick = { onUsePassword() },
shape = RectangleShape,
colors = ButtonDefaults.buttonColors(backgroundColor = colors.confirmButton),
modifier = Modifier.height(48.dp),
) {
Text(
text = stringResource(id = R.string.mozac_feature_prompts_suggest_strong_password_use_password),
style = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
color = Color.White,
letterSpacing = 0.15.sp,
fontWeight = FontWeight.Bold,
),
)
}
}
}
@Composable
@Preview
private fun GenerateStrongPasswordDialogPreview() {
DialogPreviewMaterialTheme {
PasswordGeneratorBottomSheet(
generatedStrongPassword = "StrongPassword123#",
onUsePassword = {},
onCancelDialog = {},
)
}
}

View File

@ -0,0 +1,73 @@
/* 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 mozilla.components.feature.prompts.login
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
* Creates a [PasswordGeneratorDialogColors] that represents the default colors used in an
* Password Generator bottom sheet dialog.
*
* @param title The text color for the title of the dialog.
* @param description The text color for the description.
* @param background The background color of the dialog.
* @param confirmButton The color of the confirmation dialog.
* @param passwordBox The color of the box that contains the generated password.
* @param boxBorder The border color of the box that contains the generated password.
*/
data class PasswordGeneratorDialogColors(
val title: Color,
val description: Color,
val background: Color,
val confirmButton: Color,
val passwordBox: Color,
val boxBorder: Color,
) {
companion object {
/**
* @see [PasswordGeneratorDialogColors]
*/
@Composable
fun default(
title: Color = MaterialTheme.colors.onBackground,
description: Color = MaterialTheme.colors.onBackground.copy(
alpha = ContentAlpha.medium,
),
background: Color = MaterialTheme.colors.primary,
confirmButton: Color = MaterialTheme.colors.primary,
passwordBox: Color = MaterialTheme.colors.primary,
boxBorder: Color = MaterialTheme.colors.primary,
) = PasswordGeneratorDialogColors(
title = title,
description = description,
background = background,
confirmButton = confirmButton,
passwordBox = passwordBox,
boxBorder = boxBorder,
)
/**
* Creates a provider that provides the default [PasswordGeneratorDialogColors]
*/
fun defaultProvider() = PasswordGeneratorDialogColorsProvider { default() }
}
}
/**
* An [PasswordGeneratorDialogColorsProvider] implementation can provide an [PasswordGeneratorDialogColors]
*/
fun interface PasswordGeneratorDialogColorsProvider {
/**
* Provides [PasswordGeneratorDialogColors]
*/
@Composable
fun provideColors(): PasswordGeneratorDialogColors
}

View File

@ -0,0 +1,143 @@
/* 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 mozilla.components.feature.prompts.login
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import mozilla.components.concept.storage.Login
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.dialog.KEY_PROMPT_UID
import mozilla.components.feature.prompts.dialog.KEY_SESSION_ID
import mozilla.components.feature.prompts.dialog.PromptDialogFragment
import mozilla.components.support.utils.ext.getParcelableCompat
private const val GENERATED_PASSWORD = "GENERATED_PASSWORD"
private const val URL = "URL"
/**
* Defines a dialog for suggesting a strong generated password when creating a
* new account on a website
*/
internal class PasswordGeneratorDialogFragment : PromptDialogFragment() {
private val generatedPassword: String? by lazy {
safeArguments.getParcelableCompat(GENERATED_PASSWORD, String::class.java)!!
}
private val currentUrl: String? by lazy {
safeArguments.getParcelableCompat(URL, String::class.java)!!
}
private var onSavedGeneratedPassword: () -> Unit = {}
private var colorsProvider: PasswordGeneratorDialogColorsProvider =
PasswordGeneratorDialogColors.defaultProvider()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return BottomSheetDialog(requireContext(), R.style.MozDialogStyle).apply {
setCancelable(true)
setOnShowListener {
val bottomSheet =
findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.peekHeight = resources.displayMetrics.heightPixels
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
MaterialTheme(colors) {
if (generatedPassword != null && currentUrl != null) {
PasswordGeneratorBottomSheet(
generatedStrongPassword = generatedPassword!!,
onUsePassword = {
onUsePassword(
generatedPassword = generatedPassword!!,
currentUrl = currentUrl!!,
)
},
onCancelDialog = { onCancelDialog() },
colors = colorsProvider.provideColors(),
)
}
}
}
}
/**
* Called when a generated password is being used when creating a new account on a website.
*/
@VisibleForTesting
internal fun onUsePassword(generatedPassword: String, currentUrl: String) {
val login = Login(
guid = "",
origin = currentUrl,
formActionOrigin = currentUrl,
httpRealm = currentUrl,
username = "",
password = generatedPassword,
)
feature?.onConfirm(sessionId, promptRequestUID, login)
dismiss()
onSavedGeneratedPassword.invoke()
}
@VisibleForTesting
internal fun onCancelDialog() {
feature?.onCancel(sessionId, promptRequestUID)
dismiss()
}
companion object {
/**
* A builder method for creating a [PasswordGeneratorDialogFragment]
* @param sessionId The id of the session for which this dialog will be created.
* @param promptRequestUID Identifier of the PromptRequest for which this dialog is shown.
* @param generatedPassword The strong generated password.
* @param currentUrl The url for which the strong password is generated.
* @param colorsProvider The color provider for the password generator bottom sheet.
*/
fun newInstance(
sessionId: String,
promptRequestUID: String,
generatedPassword: String,
currentUrl: String,
onSavedGeneratedPassword: () -> Unit,
colorsProvider: PasswordGeneratorDialogColorsProvider,
) = PasswordGeneratorDialogFragment().apply {
arguments = (arguments ?: Bundle()).apply {
putString(KEY_SESSION_ID, sessionId)
putString(KEY_PROMPT_UID, promptRequestUID)
putString(GENERATED_PASSWORD, generatedPassword)
putString(URL, currentUrl)
}
this.onSavedGeneratedPassword = onSavedGeneratedPassword
this.colorsProvider = colorsProvider
}
}
}

View File

@ -0,0 +1,120 @@
/* 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 mozilla.components.feature.prompts.login
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
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.width
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.withStyledAttributes
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.identitycredential.previews.DialogPreviewMaterialTheme
import mozilla.components.support.ktx.android.content.getColorFromAttr
private val Context.primaryColor: Color
get() = Color(getColorFromAttr(android.R.attr.textColorPrimary))
private val Context.accentColor: Color
get() {
var color = Color.Unspecified
withStyledAttributes(null, R.styleable.LoginSelectBar) {
val resId = getResourceId(R.styleable.LoginSelectBar_mozacLoginSelectHeaderTextStyle, 0)
if (resId > 0) {
withStyledAttributes(resId, intArrayOf(android.R.attr.textColor)) {
color = Color(getColor(0, android.graphics.Color.BLACK))
}
}
}
return color
}
/**
* Colors used to theme [PasswordGeneratorPrompt]
*
* @param primary The color used for the text in [PasswordGeneratorPrompt].
* @param header The color used for the header in [PasswordGeneratorPrompt].
*/
data class PasswordGeneratorPromptColors(
val primary: Color,
val header: Color,
) {
constructor(context: Context) : this(
primary = context.primaryColor,
header = context.accentColor,
)
}
/**
* The password generator prompt
*
* @param onGeneratedPasswordPromptClick A callback invoked when the user clicks on the prompt.
* @param modifier The [Modifier] used for this view.
* @param colors The [PasswordGeneratorPromptColors] used for this view.
*/
@Composable
fun PasswordGeneratorPrompt(
onGeneratedPasswordPromptClick: () -> Unit,
modifier: Modifier = Modifier,
colors: PasswordGeneratorPromptColors,
) {
Row(
modifier = modifier
.clickable { onGeneratedPasswordPromptClick() }
.fillMaxWidth()
.height(48.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_login_24),
contentDescription = null,
tint = colors.header,
)
Spacer(Modifier.width(24.dp))
Text(
text = stringResource(id = R.string.mozac_feature_prompts_suggest_strong_password_2),
color = colors.header,
fontSize = 16.sp,
style = MaterialTheme.typography.subtitle2,
)
}
}
@Preview
@Composable
private fun PasswordGeneratorPromptPreview() {
DialogPreviewMaterialTheme {
PasswordGeneratorPrompt(
onGeneratedPasswordPromptClick = {},
colors = PasswordGeneratorPromptColors(
primary = MaterialTheme.colors.primary,
header = MaterialTheme.colors.onBackground,
),
modifier = Modifier.background(Color.White),
)
}
}

View File

@ -7,7 +7,6 @@ package mozilla.components.feature.prompts.login
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.storage.Login
import mozilla.components.feature.prompts.concept.PasswordPromptView import mozilla.components.feature.prompts.concept.PasswordPromptView
import mozilla.components.feature.prompts.consumePromptFrom import mozilla.components.feature.prompts.consumePromptFrom
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
@ -27,22 +26,14 @@ internal class StrongPasswordPromptViewListener(
private var sessionId: String? = null, private var sessionId: String? = null,
) : PasswordPromptView.Listener { ) : PasswordPromptView.Listener {
var onGeneratedPasswordPromptClick: () -> Unit = { }
init { init {
suggestStrongPasswordBar.listener = this suggestStrongPasswordBar.listener = this
} }
internal fun handleSuggestStrongPasswordRequest( internal fun handleSuggestStrongPasswordRequest() {
request: PromptRequest.SelectLoginPrompt, suggestStrongPasswordBar.showPrompt()
currentUrl: String,
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
) {
request.generatedPassword?.let {
suggestStrongPasswordBar.showPrompt(
it,
currentUrl,
onSaveLoginWithStrongPassword,
)
}
} }
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
@ -71,22 +62,7 @@ internal class StrongPasswordPromptViewListener(
suggestStrongPasswordBar.hidePrompt() suggestStrongPasswordBar.hidePrompt()
} }
override fun onUseGeneratedPassword( override fun onGeneratedPasswordPromptClick() {
generatedPassword: String, onGeneratedPasswordPromptClick.invoke()
url: String,
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
) {
browserStore.consumePromptFrom<PromptRequest.SelectLoginPrompt>(sessionId) {
// Create complete login entry: https://bugzilla.mozilla.org/show_bug.cgi?id=1869575
val createdLoginEntryWithPassword = Login(
guid = "",
origin = url,
username = "",
password = generatedPassword,
)
it.onConfirm(createdLoginEntryWithPassword)
}
onSaveLoginWithStrongPassword.invoke(url, generatedPassword)
suggestStrongPasswordBar.hidePrompt()
} }
} }

View File

@ -5,15 +5,10 @@
package mozilla.components.feature.prompts.login package mozilla.components.feature.prompts.login
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import androidx.compose.runtime.Composable
import androidx.appcompat.widget.AppCompatTextView import androidx.compose.ui.platform.AbstractComposeView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.withStyledAttributes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.TextViewCompat
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.concept.PasswordPromptView import mozilla.components.feature.prompts.concept.PasswordPromptView
/** /**
@ -23,88 +18,23 @@ class SuggestStrongPasswordBar @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleAttr: Int = 0,
) : ConstraintLayout(context, attrs, defStyleAttr), PasswordPromptView { ) : AbstractComposeView(context, attrs, defStyleAttr), PasswordPromptView {
private var headerTextStyle: Int? = null
private var suggestStrongPasswordView: View? = null
private var suggestStrongPasswordHeader: AppCompatTextView? = null
private var useStrongPasswordTitle: AppCompatTextView? = null
override var listener: PasswordPromptView.Listener? = null override var listener: PasswordPromptView.Listener? = null
private val colors = PasswordGeneratorPromptColors(context)
init { @Composable
context.withStyledAttributes( override fun Content() {
attrs, PasswordGeneratorPrompt(
R.styleable.LoginSelectBar, onGeneratedPasswordPromptClick = { listener?.onGeneratedPasswordPromptClick() },
defStyleAttr, colors = colors,
0, )
) {
val textStyle =
getResourceId(R.styleable.LoginSelectBar_mozacLoginSelectHeaderTextStyle, 0)
if (textStyle > 0) {
headerTextStyle = textStyle
}
}
} }
override fun showPrompt( override fun showPrompt() {
generatedPassword: String, isVisible = true
url: String,
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
) {
if (suggestStrongPasswordView == null) {
suggestStrongPasswordView =
View.inflate(context, R.layout.mozac_feature_suggest_strong_password_view, this)
bindViews(generatedPassword, url, onSaveLoginWithStrongPassword)
}
suggestStrongPasswordView?.isVisible = true
useStrongPasswordTitle?.isVisible = false
} }
override fun hidePrompt() { override fun hidePrompt() {
isVisible = false isVisible = false
} }
private fun bindViews(
strongPassword: String,
url: String,
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
) {
suggestStrongPasswordHeader =
findViewById<AppCompatTextView>(R.id.suggest_strong_password_header).apply {
headerTextStyle?.let {
TextViewCompat.setTextAppearance(this, it)
currentTextColor.let { textColor ->
TextViewCompat.setCompoundDrawableTintList(
this,
ColorStateList.valueOf(textColor),
)
}
}
setOnClickListener {
useStrongPasswordTitle?.let {
it.visibility = if (it.isVisible) {
GONE
} else {
VISIBLE
}
}
}
}
useStrongPasswordTitle = findViewById<AppCompatTextView>(R.id.use_strong_password).apply {
text = context.getString(
R.string.mozac_feature_prompts_suggest_strong_password_message,
strongPassword,
)
visibility = GONE
setOnClickListener {
listener?.onUseGeneratedPassword(
generatedPassword = strongPassword,
url = url,
onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
)
}
}
}
} }

View File

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/suggest_strong_password_header"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:contentDescription="@string/mozac_feature_prompts_suggest_strong_password_content_description"
android:drawablePadding="24dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="56dp"
android:text="@string/mozac_feature_prompts_suggest_strong_password"
android:textColor="?android:colorEdgeEffect"
android:textSize="16sp"
app:drawableStartCompat="@drawable/mozac_ic_login_24"
app:drawableTint="?android:colorEdgeEffect"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/use_strong_password"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:drawablePadding="24dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="0dp"
android:text="@string/mozac_feature_prompts_suggest_strong_password_message"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
android:visibility="gone"
app:drawableStartCompat="@drawable/mozac_ic_lock_24"
app:drawableTint="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/suggest_strong_password_header" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View File

@ -118,10 +118,28 @@
<!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password --> <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
<string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggest strong password</string> <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggest strong password</string>
<!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password --> <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
<string name="mozac_feature_prompts_suggest_strong_password">Suggest strong password</string> <string name="mozac_feature_prompts_suggest_strong_password" moz:removedIn="128" tools:ignore="UnusedResources">Suggest strong password</string>
<!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
<string name="mozac_feature_prompts_suggest_strong_password_2">Use strong password</string>
<!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password --> <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
<string name="mozac_feature_prompts_suggest_strong_password_message">Use strong password: %1$s</string> <string name="mozac_feature_prompts_suggest_strong_password_message">Use strong password: %1$s</string>
<!-- Title for using the suggest strong password confirmation dialog -->
<string name="mozac_feature_prompts_suggest_strong_password_title">Use strong password?</string>
<!-- Content description for the suggest strong password confirmation dialog -->
<string name="mozac_feature_prompts_suggest_strong_password_description">Protect your accounts by using a strong, randomly generated password. Get quick access to your passwords by saving it in your account.</string>
<!-- The actual suggested strong password. %1$s will be replaced with the generated password -->
<string name="mozac_feature_prompts_suggest_strong_password_generated_password">%1$s</string>
<!-- Pressing this will use the suggested strong password -->
<string name="mozac_feature_prompts_suggest_strong_password_use_password">Use password</string>
<!-- Pressing this will dismiss the suggested strong password dialog -->
<string name="mozac_feature_prompts_suggest_strong_password_dismiss">Not now</string>
<!-- Title for showing the suggest strong password saved confirmation snackbar -->
<string name="mozac_feature_prompts_suggest_strong_password_saved_snackbar_title">Password saved</string>
<!-- Title for showing the suggest strong password updated confirmation snackbar -->
<string name="mozac_feature_prompts_suggest_strong_password_updated_snackbar_title">Password updated</string>
<!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages --> <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
<string name="mozac_feature_prompt_repost_title">Resend data to this site?</string> <string name="mozac_feature_prompt_repost_title">Resend data to this site?</string>
<string name="mozac_feature_prompt_repost_message">Refreshing this page could duplicate recent actions, such as sending a payment or posting a comment twice.</string> <string name="mozac_feature_prompt_repost_message">Refreshing this page could duplicate recent actions, such as sending a payment or posting a comment twice.</string>

View File

@ -0,0 +1,49 @@
/* 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 mozilla.components.feature.prompts.login
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertEquals
import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.mock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations.openMocks
@RunWith(AndroidJUnit4::class)
class PasswordGeneratorDialogFragmentTest {
private lateinit var fragment: PasswordGeneratorDialogFragment
@Before
fun setup() {
openMocks(this)
fragment = spy(
PasswordGeneratorDialogFragment.newInstance(
sessionId = "sessionId",
promptRequestUID = "uid",
generatedPassword = "StrongPassword123#",
currentUrl = "https://www.mozilla.org",
onSavedGeneratedPassword = {},
colorsProvider = mock(),
),
)
}
@Test
fun `build dialog`() {
doReturn(appCompatContext).`when`(fragment).requireContext()
val dialog = fragment.onCreateDialog(null)
dialog.show()
assertEquals(fragment.sessionId, "sessionId")
assertEquals(fragment.promptRequestUID, "uid")
assertEquals(dialog.isShowing, true)
}
}

View File

@ -53,8 +53,7 @@ class StrongPasswordPromptViewListenerTest {
private lateinit var suggestStrongPasswordPromptViewListener: StrongPasswordPromptViewListener private lateinit var suggestStrongPasswordPromptViewListener: StrongPasswordPromptViewListener
private lateinit var suggestStrongPasswordBar: SuggestStrongPasswordBar private lateinit var suggestStrongPasswordBar: SuggestStrongPasswordBar
private val onSaveLoginWithGeneratedPass: (String, String) -> Unit = mock() private val onGeneratedPasswordPromptClick: () -> Unit = mock()
private val url = "https://www.mozilla.org"
@Before @Before
fun setup() { fun setup() {
@ -79,25 +78,18 @@ class StrongPasswordPromptViewListenerTest {
whenever(state.customTabs).thenReturn(listOf(customTab)) whenever(state.customTabs).thenReturn(listOf(customTab))
suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar) suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, url, onSaveLoginWithGeneratedPass) suggestStrongPasswordPromptViewListener.onGeneratedPasswordPromptClick = onGeneratedPasswordPromptClick
Mockito.verify(suggestStrongPasswordBar).showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass) suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest()
Mockito.verify(suggestStrongPasswordBar).showPrompt()
} }
@Test @Test
fun `StrongPasswordGenerator shows the suggest strong password bar on a selected tab`() { fun `StrongPasswordGenerator shows the suggest strong password bar on a selected tab`() {
prepareSelectedSession(request) prepareSelectedSession(request)
suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar) suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, url, onSaveLoginWithGeneratedPass) suggestStrongPasswordPromptViewListener.onGeneratedPasswordPromptClick = onGeneratedPasswordPromptClick
Mockito.verify(suggestStrongPasswordBar).showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass) suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest()
} Mockito.verify(suggestStrongPasswordBar).showPrompt()
@Test
fun `StrongPasswordGenerator invokes use the suggested password and hides view`() {
prepareSelectedSession(request)
suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, "") { _, _ -> }
suggestStrongPasswordPromptViewListener.onUseGeneratedPassword(suggestedPassword, "") { _, _ -> }
Mockito.verify(suggestStrongPasswordBar).hidePrompt()
} }
@Test @Test

View File

@ -5,11 +5,8 @@
package mozilla.components.feature.prompts.login package mozilla.components.feature.prompts.login
import android.view.View import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.concept.PasswordPromptView import mozilla.components.feature.prompts.concept.PasswordPromptView
import mozilla.components.support.test.ext.appCompatContext import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.mock import mozilla.components.support.test.mock
@ -29,33 +26,14 @@ class SuggestStrongPasswordBarTest {
} }
@Test @Test
fun `listener is invoked when clicking use strong password option`() { fun `show prompt updates visibility`() {
val bar = SuggestStrongPasswordBar(appCompatContext) val bar = SuggestStrongPasswordBar(appCompatContext)
val listener: PasswordPromptView.Listener = mock() val listener: PasswordPromptView.Listener = mock()
val suggestedPassword = "generatedPassword123#"
val url = "https://wwww.abc.com"
val onSaveLoginWithGeneratedPass: (String, String) -> Unit = mock()
Assert.assertNull(bar.listener) Assert.assertNull(bar.listener)
bar.listener = listener bar.listener = listener
bar.showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass) Assert.assertNotNull(bar.listener)
bar.findViewById<AppCompatTextView>(R.id.use_strong_password).performClick()
Mockito.verify(listener)
.onUseGeneratedPassword(suggestedPassword, url, onSaveLoginWithGeneratedPass)
}
@Test bar.showPrompt()
fun `view is expanded when clicking header`() { Assert.assertTrue(bar.isVisible)
val bar = SuggestStrongPasswordBar(appCompatContext)
val suggestedPassword = "generatedPassword123#"
bar.showPrompt(suggestedPassword, "") { _, _ -> }
bar.findViewById<AppCompatTextView>(R.id.suggest_strong_password_header).performClick()
// Expanded
Assert.assertTrue(bar.findViewById<RecyclerView>(R.id.use_strong_password).isVisible)
bar.findViewById<AppCompatTextView>(R.id.suggest_strong_password_header).performClick()
// Hidden
Assert.assertFalse(bar.findViewById<RecyclerView>(R.id.use_strong_password).isVisible)
} }
} }

View File

@ -87,6 +87,8 @@ import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog
import mozilla.components.feature.prompts.identitycredential.DialogColors import mozilla.components.feature.prompts.identitycredential.DialogColors
import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider
import mozilla.components.feature.prompts.login.LoginDelegate import mozilla.components.feature.prompts.login.LoginDelegate
import mozilla.components.feature.prompts.login.PasswordGeneratorDialogColors
import mozilla.components.feature.prompts.login.PasswordGeneratorDialogColorsProvider
import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate
import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.feature.prompts.share.ShareDelegate
import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.ReaderViewFeature
@ -684,6 +686,17 @@ abstract class BaseBrowserFragment :
}, },
) )
val passwordGeneratorColorsProvider = PasswordGeneratorDialogColorsProvider {
PasswordGeneratorDialogColors(
title = ThemeManager.resolveAttributeColor(attribute = R.attr.textPrimary),
description = ThemeManager.resolveAttributeColor(attribute = R.attr.textSecondary),
background = ThemeManager.resolveAttributeColor(attribute = R.attr.layer1),
confirmButton = ThemeManager.resolveAttributeColor(attribute = R.attr.actionPrimary),
passwordBox = ThemeManager.resolveAttributeColor(attribute = R.attr.layer2),
boxBorder = ThemeManager.resolveAttributeColor(attribute = R.attr.textDisabled),
)
}
val bottomToolbarHeight = context.settings().getBottomToolbarHeight() val bottomToolbarHeight = context.settings().getBottomToolbarHeight()
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
@ -829,6 +842,10 @@ abstract class BaseBrowserFragment :
get() = binding.suggestStrongPasswordBar get() = binding.suggestStrongPasswordBar
}, },
isSuggestStrongPasswordEnabled = context.settings().enableSuggestStrongPassword, isSuggestStrongPasswordEnabled = context.settings().enableSuggestStrongPassword,
shouldAutomaticallyShowSuggestedPassword = { context.settings().isFirstTimeEngagingWithSignup },
onFirstTimeEngagedWithSignup = {
context.settings().isFirstTimeEngagingWithSignup = false
},
onSaveLoginWithStrongPassword = { url, password -> onSaveLoginWithStrongPassword = { url, password ->
handleOnSaveLoginWithGeneratedStrongPassword( handleOnSaveLoginWithGeneratedStrongPassword(
passwordsStorage = context.components.core.passwordsStorage, passwordsStorage = context.components.core.passwordsStorage,
@ -836,6 +853,10 @@ abstract class BaseBrowserFragment :
password = password, password = password,
) )
}, },
onSavedGeneratedPassword = {
showSnackbarAfterUsingTheGeneratedPassword()
},
passwordGeneratorColorsProvider = passwordGeneratorColorsProvider,
creditCardDelegate = object : CreditCardDelegate { creditCardDelegate = object : CreditCardDelegate {
override val creditCardPickerView override val creditCardPickerView
get() = binding.creditCardSelectBar get() = binding.creditCardSelectBar
@ -1071,6 +1092,17 @@ abstract class BaseBrowserFragment :
} }
} }
/**
* Show a [Snackbar] when credentials are saved using the generated password.
*/
private fun showSnackbarAfterUsingTheGeneratedPassword() {
FenixSnackbarDelegate(binding.dynamicSnackbarContainer).show(
snackBarParentView = binding.dynamicSnackbarContainer,
text = R.string.mozac_feature_prompts_suggest_strong_password_saved_snackbar_title,
duration = Snackbar.LENGTH_LONG,
)
}
/** /**
* Shows a biometric prompt and fallback to prompting for the password. * Shows a biometric prompt and fallback to prompting for the password.
*/ */

View File

@ -1952,6 +1952,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
featureFlag = FeatureFlags.suggestStrongPassword, featureFlag = FeatureFlags.suggestStrongPassword,
) )
/**
* Indicates first time engaging with signup
*/
var isFirstTimeEngagingWithSignup: Boolean by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_first_time_engage_with_signup),
default = true,
)
/** /**
* Indicates if the user has chosen to show sponsored search suggestions in the awesomebar. * Indicates if the user has chosen to show sponsored search suggestions in the awesomebar.
* The default value is computed lazily, and based on whether Firefox Suggest is enabled. * The default value is computed lazily, and based on whether Firefox Suggest is enabled.

View File

@ -108,6 +108,8 @@
<string name="pref_key_sync_credit_cards" translatable="false">pref_key_sync_credit_cards</string> <string name="pref_key_sync_credit_cards" translatable="false">pref_key_sync_credit_cards</string>
<!-- Key for Address sync preference in the account settings fragment --> <!-- Key for Address sync preference in the account settings fragment -->
<string name="pref_key_sync_address" translatable="false">pref_key_sync_address</string> <string name="pref_key_sync_address" translatable="false">pref_key_sync_address</string>
<!-- Key for engaging first time with signup -->
<string name="pref_key_first_time_engage_with_signup" translatable="false">pref_key_first_time_engage_with_signup</string>
<!-- Search Settings --> <!-- Search Settings -->
<string name="pref_key_search_engine_list" translatable="false">pref_key_search_engine_list</string> <string name="pref_key_search_engine_list" translatable="false">pref_key_search_engine_list</string>

View File

@ -690,6 +690,10 @@ export const GeckoViewAutocomplete = {
debug`delegateSelection - filling form`; debug`delegateSelection - filling form`;
if (selectedOption.hint & SelectOption.Hint.GENERATED) {
this.onLoginSave(selectedLogin);
}
const actor = const actor =
browsingContext.currentWindowGlobal.getActor("LoginManager"); browsingContext.currentWindowGlobal.getActor("LoginManager");