mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 09:54:42 +00:00
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:
parent
f114f4f423
commit
a6c628a00d
@ -191,7 +191,7 @@ sealed class PromptRequest(
|
||||
* 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 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.
|
||||
*/
|
||||
data class SelectLoginPrompt(
|
||||
|
@ -46,6 +46,7 @@ import mozilla.components.concept.identitycredential.Account
|
||||
import mozilla.components.concept.identitycredential.Provider
|
||||
import mozilla.components.concept.storage.CreditCardEntry
|
||||
import mozilla.components.concept.storage.CreditCardValidationDelegate
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.concept.storage.LoginEntry
|
||||
import mozilla.components.concept.storage.LoginValidationDelegate
|
||||
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.LoginExceptions
|
||||
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.SuggestStrongPasswordDelegate
|
||||
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.
|
||||
* @property onSaveLoginWithStrongPassword A callback invoked to save a new login that uses the
|
||||
* 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 addressDelegate Delegate for address picker.
|
||||
* @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.
|
||||
* Once the request is completed, [onPermissionsResult] needs to be invoked.
|
||||
*/
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
@Suppress("LargeClass", "LongParameterList", "MaxLineLength")
|
||||
class PromptFeature private constructor(
|
||||
private val container: PromptContainer,
|
||||
private val store: BrowserStore,
|
||||
@ -184,7 +192,13 @@ class PromptFeature private constructor(
|
||||
private val suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
|
||||
SuggestStrongPasswordDelegate {},
|
||||
private val isSuggestStrongPasswordEnabled: Boolean = false,
|
||||
private var shouldAutomaticallyShowSuggestedPassword: () -> Boolean = { false },
|
||||
private val onFirstTimeEngagedWithSignup: () -> 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 addressDelegate: AddressDelegate = DefaultAddressDelegate(),
|
||||
private val fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
@ -233,7 +247,13 @@ class PromptFeature private constructor(
|
||||
suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
|
||||
SuggestStrongPasswordDelegate {},
|
||||
isSuggestStrongPasswordEnabled: Boolean = false,
|
||||
shouldAutomaticallyShowSuggestedPassword: () -> Boolean = { false },
|
||||
onFirstTimeEngagedWithSignup: () -> Unit = {},
|
||||
onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
|
||||
onSavedGeneratedPassword: () -> Unit = {},
|
||||
passwordGeneratorColorsProvider: PasswordGeneratorDialogColorsProvider = PasswordGeneratorDialogColorsProvider {
|
||||
PasswordGeneratorDialogColors.default()
|
||||
},
|
||||
creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
|
||||
addressDelegate: AddressDelegate = DefaultAddressDelegate(),
|
||||
fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
@ -259,7 +279,11 @@ class PromptFeature private constructor(
|
||||
loginDelegate = loginDelegate,
|
||||
suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
|
||||
isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
|
||||
shouldAutomaticallyShowSuggestedPassword = shouldAutomaticallyShowSuggestedPassword,
|
||||
onFirstTimeEngagedWithSignup = onFirstTimeEngagedWithSignup,
|
||||
onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
|
||||
onSavedGeneratedPassword = onSavedGeneratedPassword,
|
||||
passwordGeneratorColorsProvider = passwordGeneratorColorsProvider,
|
||||
creditCardDelegate = creditCardDelegate,
|
||||
addressDelegate = addressDelegate,
|
||||
)
|
||||
@ -283,7 +307,10 @@ class PromptFeature private constructor(
|
||||
suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
|
||||
SuggestStrongPasswordDelegate {},
|
||||
isSuggestStrongPasswordEnabled: Boolean = false,
|
||||
shouldAutomaticallyShowSuggestedPassword: () -> Boolean = { false },
|
||||
onFirstTimeEngagedWithSignup: () -> Unit = {},
|
||||
onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
|
||||
onSavedGeneratedPassword: () -> Unit = {},
|
||||
creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
|
||||
addressDelegate: AddressDelegate = DefaultAddressDelegate(),
|
||||
fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
@ -308,7 +335,10 @@ class PromptFeature private constructor(
|
||||
loginDelegate = loginDelegate,
|
||||
suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
|
||||
isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
|
||||
shouldAutomaticallyShowSuggestedPassword = shouldAutomaticallyShowSuggestedPassword,
|
||||
onFirstTimeEngagedWithSignup = onFirstTimeEngagedWithSignup,
|
||||
onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
|
||||
onSavedGeneratedPassword = onSavedGeneratedPassword,
|
||||
creditCardDelegate = creditCardDelegate,
|
||||
addressDelegate = addressDelegate,
|
||||
)
|
||||
@ -572,14 +602,20 @@ class PromptFeature private constructor(
|
||||
return
|
||||
}
|
||||
if (promptRequest.generatedPassword != null && isSuggestStrongPasswordEnabled) {
|
||||
val currentUrl =
|
||||
store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.content?.url
|
||||
if (currentUrl != null) {
|
||||
strongPasswordPromptViewListener?.handleSuggestStrongPasswordRequest(
|
||||
if (shouldAutomaticallyShowSuggestedPassword.invoke()) {
|
||||
onFirstTimeEngagedWithSignup.invoke()
|
||||
handleDialogsRequest(
|
||||
promptRequest,
|
||||
currentUrl,
|
||||
onSaveLoginWithStrongPassword,
|
||||
session,
|
||||
)
|
||||
} else {
|
||||
strongPasswordPromptViewListener?.onGeneratedPasswordPromptClick = {
|
||||
handleDialogsRequest(
|
||||
promptRequest,
|
||||
session,
|
||||
)
|
||||
}
|
||||
strongPasswordPromptViewListener?.handleSuggestStrongPasswordRequest()
|
||||
}
|
||||
} else {
|
||||
loginPicker?.handleSelectLoginRequest(promptRequest)
|
||||
@ -694,6 +730,7 @@ class PromptFeature private constructor(
|
||||
is PromptRequest.IdentityCredential.SelectProvider -> it.onConfirm(value as Provider)
|
||||
is PromptRequest.IdentityCredential.SelectAccount -> it.onConfirm(value as Account)
|
||||
is PromptRequest.IdentityCredential.PrivacyPolicy -> it.onConfirm(value as Boolean)
|
||||
is SelectLoginPrompt -> it.onConfirm(value as Login)
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
@ -760,6 +797,29 @@ class PromptFeature private constructor(
|
||||
) {
|
||||
// Requests that are handled with dialogs
|
||||
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 -> {
|
||||
if (!isCreditCardAutofillEnabled.invoke() || creditCardValidationDelegate == null ||
|
||||
!promptRequest.creditCard.isValid
|
||||
|
@ -12,13 +12,9 @@ interface PasswordPromptView {
|
||||
var listener: Listener?
|
||||
|
||||
/**
|
||||
* Shows a simple prompt with the given [generatedPassword].
|
||||
* Shows a simple prompt for using a generated password.
|
||||
*/
|
||||
fun showPrompt(
|
||||
generatedPassword: String,
|
||||
url: String,
|
||||
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
|
||||
)
|
||||
fun showPrompt()
|
||||
|
||||
/**
|
||||
* Hides the prompt.
|
||||
@ -30,13 +26,8 @@ interface PasswordPromptView {
|
||||
*/
|
||||
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(
|
||||
generatedPassword: String,
|
||||
url: String,
|
||||
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
|
||||
)
|
||||
fun onGeneratedPasswordPromptClick()
|
||||
}
|
||||
}
|
||||
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ package mozilla.components.feature.prompts.login
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
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.consumePromptFrom
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
@ -27,22 +26,14 @@ internal class StrongPasswordPromptViewListener(
|
||||
private var sessionId: String? = null,
|
||||
) : PasswordPromptView.Listener {
|
||||
|
||||
var onGeneratedPasswordPromptClick: () -> Unit = { }
|
||||
|
||||
init {
|
||||
suggestStrongPasswordBar.listener = this
|
||||
}
|
||||
|
||||
internal fun handleSuggestStrongPasswordRequest(
|
||||
request: PromptRequest.SelectLoginPrompt,
|
||||
currentUrl: String,
|
||||
onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
|
||||
) {
|
||||
request.generatedPassword?.let {
|
||||
suggestStrongPasswordBar.showPrompt(
|
||||
it,
|
||||
currentUrl,
|
||||
onSaveLoginWithStrongPassword,
|
||||
)
|
||||
}
|
||||
internal fun handleSuggestStrongPasswordRequest() {
|
||||
suggestStrongPasswordBar.showPrompt()
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
@ -71,22 +62,7 @@ internal class StrongPasswordPromptViewListener(
|
||||
suggestStrongPasswordBar.hidePrompt()
|
||||
}
|
||||
|
||||
override fun onUseGeneratedPassword(
|
||||
generatedPassword: String,
|
||||
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()
|
||||
override fun onGeneratedPasswordPromptClick() {
|
||||
onGeneratedPasswordPromptClick.invoke()
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,10 @@
|
||||
package mozilla.components.feature.prompts.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import mozilla.components.feature.prompts.R
|
||||
import mozilla.components.feature.prompts.concept.PasswordPromptView
|
||||
|
||||
/**
|
||||
@ -23,88 +18,23 @@ class SuggestStrongPasswordBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), PasswordPromptView {
|
||||
|
||||
private var headerTextStyle: Int? = null
|
||||
private var suggestStrongPasswordView: View? = null
|
||||
private var suggestStrongPasswordHeader: AppCompatTextView? = null
|
||||
private var useStrongPasswordTitle: AppCompatTextView? = null
|
||||
|
||||
) : AbstractComposeView(context, attrs, defStyleAttr), PasswordPromptView {
|
||||
override var listener: PasswordPromptView.Listener? = null
|
||||
private val colors = PasswordGeneratorPromptColors(context)
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.LoginSelectBar,
|
||||
defStyleAttr,
|
||||
0,
|
||||
) {
|
||||
val textStyle =
|
||||
getResourceId(R.styleable.LoginSelectBar_mozacLoginSelectHeaderTextStyle, 0)
|
||||
if (textStyle > 0) {
|
||||
headerTextStyle = textStyle
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
override fun Content() {
|
||||
PasswordGeneratorPrompt(
|
||||
onGeneratedPasswordPromptClick = { listener?.onGeneratedPasswordPromptClick() },
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
|
||||
override fun showPrompt(
|
||||
generatedPassword: String,
|
||||
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 showPrompt() {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
override fun hidePrompt() {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -118,10 +118,28 @@
|
||||
<!-- 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>
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -53,8 +53,7 @@ class StrongPasswordPromptViewListenerTest {
|
||||
private lateinit var suggestStrongPasswordPromptViewListener: StrongPasswordPromptViewListener
|
||||
private lateinit var suggestStrongPasswordBar: SuggestStrongPasswordBar
|
||||
|
||||
private val onSaveLoginWithGeneratedPass: (String, String) -> Unit = mock()
|
||||
private val url = "https://www.mozilla.org"
|
||||
private val onGeneratedPasswordPromptClick: () -> Unit = mock()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@ -79,25 +78,18 @@ class StrongPasswordPromptViewListenerTest {
|
||||
whenever(state.customTabs).thenReturn(listOf(customTab))
|
||||
|
||||
suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
|
||||
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, url, onSaveLoginWithGeneratedPass)
|
||||
Mockito.verify(suggestStrongPasswordBar).showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass)
|
||||
suggestStrongPasswordPromptViewListener.onGeneratedPasswordPromptClick = onGeneratedPasswordPromptClick
|
||||
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest()
|
||||
Mockito.verify(suggestStrongPasswordBar).showPrompt()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `StrongPasswordGenerator shows the suggest strong password bar on a selected tab`() {
|
||||
prepareSelectedSession(request)
|
||||
suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
|
||||
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, url, onSaveLoginWithGeneratedPass)
|
||||
Mockito.verify(suggestStrongPasswordBar).showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass)
|
||||
}
|
||||
|
||||
@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()
|
||||
suggestStrongPasswordPromptViewListener.onGeneratedPasswordPromptClick = onGeneratedPasswordPromptClick
|
||||
suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest()
|
||||
Mockito.verify(suggestStrongPasswordBar).showPrompt()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -5,11 +5,8 @@
|
||||
package mozilla.components.feature.prompts.login
|
||||
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import mozilla.components.feature.prompts.R
|
||||
import mozilla.components.feature.prompts.concept.PasswordPromptView
|
||||
import mozilla.components.support.test.ext.appCompatContext
|
||||
import mozilla.components.support.test.mock
|
||||
@ -29,33 +26,14 @@ class SuggestStrongPasswordBarTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `listener is invoked when clicking use strong password option`() {
|
||||
fun `show prompt updates visibility`() {
|
||||
val bar = SuggestStrongPasswordBar(appCompatContext)
|
||||
val listener: PasswordPromptView.Listener = mock()
|
||||
val suggestedPassword = "generatedPassword123#"
|
||||
val url = "https://wwww.abc.com"
|
||||
val onSaveLoginWithGeneratedPass: (String, String) -> Unit = mock()
|
||||
Assert.assertNull(bar.listener)
|
||||
bar.listener = listener
|
||||
bar.showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass)
|
||||
bar.findViewById<AppCompatTextView>(R.id.use_strong_password).performClick()
|
||||
Mockito.verify(listener)
|
||||
.onUseGeneratedPassword(suggestedPassword, url, onSaveLoginWithGeneratedPass)
|
||||
}
|
||||
Assert.assertNotNull(bar.listener)
|
||||
|
||||
@Test
|
||||
fun `view is expanded when clicking header`() {
|
||||
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)
|
||||
bar.showPrompt()
|
||||
Assert.assertTrue(bar.isVisible)
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,8 @@ import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog
|
||||
import mozilla.components.feature.prompts.identitycredential.DialogColors
|
||||
import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider
|
||||
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.share.ShareDelegate
|
||||
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()
|
||||
|
||||
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
|
||||
@ -829,6 +842,10 @@ abstract class BaseBrowserFragment :
|
||||
get() = binding.suggestStrongPasswordBar
|
||||
},
|
||||
isSuggestStrongPasswordEnabled = context.settings().enableSuggestStrongPassword,
|
||||
shouldAutomaticallyShowSuggestedPassword = { context.settings().isFirstTimeEngagingWithSignup },
|
||||
onFirstTimeEngagedWithSignup = {
|
||||
context.settings().isFirstTimeEngagingWithSignup = false
|
||||
},
|
||||
onSaveLoginWithStrongPassword = { url, password ->
|
||||
handleOnSaveLoginWithGeneratedStrongPassword(
|
||||
passwordsStorage = context.components.core.passwordsStorage,
|
||||
@ -836,6 +853,10 @@ abstract class BaseBrowserFragment :
|
||||
password = password,
|
||||
)
|
||||
},
|
||||
onSavedGeneratedPassword = {
|
||||
showSnackbarAfterUsingTheGeneratedPassword()
|
||||
},
|
||||
passwordGeneratorColorsProvider = passwordGeneratorColorsProvider,
|
||||
creditCardDelegate = object : CreditCardDelegate {
|
||||
override val creditCardPickerView
|
||||
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.
|
||||
*/
|
||||
|
@ -1952,6 +1952,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
||||
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.
|
||||
* The default value is computed lazily, and based on whether Firefox Suggest is enabled.
|
||||
|
@ -108,6 +108,8 @@
|
||||
<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 -->
|
||||
<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 -->
|
||||
<string name="pref_key_search_engine_list" translatable="false">pref_key_search_engine_list</string>
|
||||
|
@ -690,6 +690,10 @@ export const GeckoViewAutocomplete = {
|
||||
|
||||
debug`delegateSelection - filling form`;
|
||||
|
||||
if (selectedOption.hint & SelectOption.Hint.GENERATED) {
|
||||
this.onLoginSave(selectedLogin);
|
||||
}
|
||||
|
||||
const actor =
|
||||
browsingContext.currentWindowGlobal.getActor("LoginManager");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user