Skip to content

Commit

Permalink
Introduce onboarding save dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
cmonfortep committed Jul 31, 2024
1 parent 05a583b commit d6de23b
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ class SecureStoreBackedAutofillStore @Inject constructor(
override var autofillDeclineCount: Int
get() = autofillPrefsStore.autofillDeclineCount
set(value) {
Timber.i("Autofill: Setting autofillDeclineCount to %d", value)
autofillPrefsStore.autofillDeclineCount = value
}

override var monitorDeclineCounts: Boolean
get() = autofillPrefsStore.monitorDeclineCounts
set(value) {
Timber.i("Autofill: Setting monitorDeclineCounts to %b", value)
autofillPrefsStore.monitorDeclineCounts = value
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding

enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName {
AUTOFILL_ONBOARDING_SAVE_PROMPT_SHOWN("autofill_logins_save_login_inline_onboarding_displayed"),
AUTOFILL_ONBOARDING_SAVE_PROMPT_DISMISSED("autofill_logins_save_login_inline_onboarding_dismissed"),
AUTOFILL_ONBOARDING_SAVE_PROMPT_SAVED("autofill_logins_save_login_inline_onboarding_confirmed"),
AUTOFILL_ONBOARDING_SAVE_PROMPT_EXCLUDE("autofill_logins_save_login_onboarding_exclude_site_confirmed"),

AUTOFILL_SAVE_LOGIN_PROMPT_SHOWN("m_autofill_logins_save_login_inline_displayed"),
AUTOFILL_SAVE_LOGIN_PROMPT_DISMISSED("m_autofill_logins_save_login_inline_dismissed"),
AUTOFILL_SAVE_LOGIN_PROMPT_SAVED("m_autofill_logins_save_login_inline_confirmed"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.favicon.FaviconManager
Expand All @@ -35,6 +38,11 @@ import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ContentAutofillSaveNewCredentialsBinding
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_USER_SELECTED_FROM_SAVE_DIALOG
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_EXCLUDE
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_SAVED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_SHOWN
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SAVE_LOGIN_PROMPT_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SAVE_LOGIN_PROMPT_SAVED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SAVE_LOGIN_PROMPT_SHOWN
Expand All @@ -44,14 +52,17 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SAVE_PASSW
import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.AutofillSavingPixelEventNames.Companion.pixelNameDialogAccepted
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.AutofillSavingPixelEventNames.Companion.pixelNameDialogDismissed
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.AutofillSavingPixelEventNames.Companion.pixelNameDialogExclude
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.AutofillSavingPixelEventNames.Companion.pixelNameDialogShown
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.AutofillSavingPixelEventNames.Companion.saveType
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.CredentialSaveType.PasswordOnly
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.CredentialSaveType.UsernameAndPassword
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.CredentialSaveType.UsernameOnly
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.DialogEvent.Accepted
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.DialogEvent.Dismissed
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.DialogEvent.Exclude
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment.DialogEvent.Shown
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsViewModel.ViewState
import com.duckduckgo.autofill.impl.ui.credential.saving.declines.AutofillDeclineCounter
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.FragmentViewModelFactory
Expand All @@ -63,6 +74,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber

Expand Down Expand Up @@ -99,6 +112,8 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
*/
private var ignoreCancellationEvents = false

private lateinit var keyFeaturesContainer: ViewGroup

private val viewModel by lazy {
ViewModelProvider(this, viewModelFactory)[AutofillSavingCredentialsViewModel::class.java]
}
Expand All @@ -122,16 +137,33 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
pixelNameDialogEvent(Shown)?.let { pixel.fire(it) }
autofillFireproofDialogSuppressor.autofillSaveOrUpdateDialogVisibilityChanged(visible = true)
viewModel.userPromptedToSaveCredentials()

val binding = ContentAutofillSaveNewCredentialsBinding.inflate(inflater, container, false)
configureViews(binding)
observeViewModel()
return binding.root
}

private fun observeViewModel() {
viewModel.viewState
.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
.onEach { viewState ->
renderViewState(viewState)
}.launchIn(lifecycleScope)
}

private fun renderViewState(viewState: ViewState) {
keyFeaturesContainer.isVisible = viewState.expandedDialog
dialog.takeIf { it is BottomSheetDialog }?.let {
(it as BottomSheetDialog).behavior.isDraggable = viewState.expandedDialog
}
pixelNameDialogEvent(Shown, viewState.expandedDialog)?.let { pixel.fire(it) }
}

private fun configureViews(binding: ContentAutofillSaveNewCredentialsBinding) {
keyFeaturesContainer = binding.keyFeaturesContainer
(dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
configureCloseButtons(binding)
configureSaveButton(binding)
Expand All @@ -141,7 +173,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
binding.saveLoginButton.setOnClickListener {
Timber.v("onSave: AutofillSavingCredentialsDialogFragment. User saved credentials")

pixelNameDialogEvent(Accepted)?.let { pixel.fire(it) }
pixelNameDialogEvent(Accepted, binding.keyFeaturesContainer.isVisible)?.let { pixel.fire(it) }

lifecycleScope.launch(dispatcherProvider.io()) {
faviconManager.persistCachedFavicon(getTabId(), getOriginalUrl())
Expand All @@ -168,7 +200,8 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre

onUserRejectedToSaveCredentials()

pixelNameDialogEvent(Dismissed)?.let { pixel.fire(it) }
val onboardingMode = this.view?.findViewById<View>(R.id.keyFeaturesContainer)?.isVisible ?: false
pixelNameDialogEvent(Dismissed, onboardingMode)?.let { pixel.fire(it) }
}

private fun onUserRejectedToSaveCredentials() {
Expand All @@ -187,6 +220,8 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
}

private fun onUserChoseNeverSaveThisSite() {
val onboardingMode = this.view?.findViewById<View>(R.id.keyFeaturesContainer)?.isVisible ?: false
pixelNameDialogEvent(Exclude, onboardingMode)?.let { pixel.fire(it) }
viewModel.addSiteToNeverSaveList(getOriginalUrl())

// this is another way to refuse saving credentials, so ensure that normal logic still runs
Expand All @@ -212,12 +247,13 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
(dialog as BottomSheetDialog).animateClosed()
}

private fun pixelNameDialogEvent(dialogEvent: DialogEvent): AutofillPixelNames? {
private fun pixelNameDialogEvent(dialogEvent: DialogEvent, onboardingMode: Boolean): AutofillPixelNames? {
val saveType = getCredentialsToSave().saveType()
return when (dialogEvent) {
is Shown -> pixelNameDialogShown(saveType)
is Dismissed -> pixelNameDialogDismissed(saveType)
is Accepted -> pixelNameDialogAccepted(saveType)
is Shown -> pixelNameDialogShown(saveType, onboardingMode)
is Dismissed -> pixelNameDialogDismissed(saveType, onboardingMode)
is Accepted -> pixelNameDialogAccepted(saveType, onboardingMode)
is Exclude -> pixelNameDialogExclude(saveType, onboardingMode)
else -> null
}
}
Expand All @@ -232,6 +268,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
object Shown : DialogEvent
object Dismissed : DialogEvent
object Accepted : DialogEvent
object Exclude : DialogEvent
}

private fun getCredentialsToSave() = arguments?.getParcelable<LoginCredentials>(CredentialSavePickerDialog.KEY_CREDENTIALS)!!
Expand Down Expand Up @@ -270,29 +307,37 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
}
}

fun pixelNameDialogShown(credentialSaveType: CredentialSaveType): AutofillPixelNames? {
fun pixelNameDialogShown(credentialSaveType: CredentialSaveType, onboardingMode: Boolean): AutofillPixelNames? {
if (onboardingMode) return AUTOFILL_ONBOARDING_SAVE_PROMPT_SHOWN
return when (credentialSaveType) {
UsernameAndPassword -> AUTOFILL_SAVE_LOGIN_PROMPT_SHOWN
PasswordOnly -> AUTOFILL_SAVE_PASSWORD_PROMPT_SHOWN
else -> null
}
}

fun pixelNameDialogDismissed(credentialSaveType: CredentialSaveType): AutofillPixelNames? {
fun pixelNameDialogDismissed(credentialSaveType: CredentialSaveType, onboardingMode: Boolean): AutofillPixelNames? {
if (onboardingMode) return AUTOFILL_ONBOARDING_SAVE_PROMPT_DISMISSED
return when (credentialSaveType) {
UsernameAndPassword -> AUTOFILL_SAVE_LOGIN_PROMPT_DISMISSED
PasswordOnly -> AUTOFILL_SAVE_PASSWORD_PROMPT_DISMISSED
else -> null
}
}

fun pixelNameDialogAccepted(credentialSaveType: CredentialSaveType): AutofillPixelNames? {
fun pixelNameDialogAccepted(credentialSaveType: CredentialSaveType, onboardingMode: Boolean): AutofillPixelNames? {
if (onboardingMode) return AUTOFILL_ONBOARDING_SAVE_PROMPT_SAVED
return when (credentialSaveType) {
UsernameAndPassword -> AUTOFILL_SAVE_LOGIN_PROMPT_SAVED
PasswordOnly -> AUTOFILL_SAVE_PASSWORD_PROMPT_SAVED
else -> null
}
}

fun pixelNameDialogExclude(saveType: CredentialSaveType, onboardingMode: Boolean): AutofillPixelNames {
if (onboardingMode) return AUTOFILL_ONBOARDING_SAVE_PROMPT_EXCLUDE
return AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_USER_SELECTED_FROM_SAVE_DIALOG
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,40 @@ package com.duckduckgo.autofill.impl.ui.credential.saving
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_USER_SELECTED_FROM_SAVE_DIALOG
import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ViewState
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.di.scopes.FragmentScope
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber

@ContributesViewModel(ActivityScope::class)
@ContributesViewModel(FragmentScope::class)
class AutofillSavingCredentialsViewModel @Inject constructor(
private val dispatchers: DispatcherProvider,
private val pixel: Pixel,
private val neverSavedSiteRepository: NeverSavedSiteRepository,
private val autofillStore: InternalAutofillStore,
) : ViewModel() {

@Inject
lateinit var autofillStore: InternalAutofillStore
private val _viewState = MutableStateFlow(ViewState())

init {
viewModelScope.launch(dispatchers.io()) {
val shouldShowExpandedView = autofillStore.autofillDeclineCount < 2 && autofillStore.monitorDeclineCounts
_viewState.value = ViewState(shouldShowExpandedView)
Timber.d("Autofill: AutofillSavingCredentialsViewModel initialized")
}
}

val viewState: Flow<ViewState> = _viewState.asStateFlow()

data class ViewState(
val expandedDialog: Boolean = true,
)

fun userPromptedToSaveCredentials() {
viewModelScope.launch(dispatchers.io()) {
Expand All @@ -46,10 +61,9 @@ class AutofillSavingCredentialsViewModel @Inject constructor(
}

fun addSiteToNeverSaveList(originalUrl: String) {
Timber.d("User selected to never save for this site %s", originalUrl)
Timber.d("Autofill: User selected to never save for this site %s", originalUrl)
viewModelScope.launch(dispatchers.io()) {
neverSavedSiteRepository.addToNeverSaveList(originalUrl)
}
pixel.fire(AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_USER_SELECTED_FROM_SAVE_DIALOG)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2023 DuckDuckGo
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:radius="@dimen/smallShapeCornerRadius"/>
<stroke
android:width="2dp"
android:color="?attr/daxColorContainer" />
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.026,8.75H5.75C4.093,8.75 2.75,10.093 2.75,11.75V17.25C2.75,18.907 4.093,20.25 5.75,20.25H18.25C19.907,20.25 21.25,18.907 21.25,17.25V11.75C21.25,11.266 21.135,10.809 20.932,10.405C20.623,10.462 20.306,10.494 19.994,10.526C19.849,10.541 19.705,10.556 19.564,10.573C19.472,11.32 19.364,12.105 19.279,12.441C19.074,13.255 18.456,14.01 17.49,13.978C16.603,13.949 15.992,13.257 15.742,12.515C15.581,12.034 15.527,11.508 15.474,10.994C15.459,10.849 15.444,10.705 15.427,10.564C14.68,10.472 13.895,10.364 13.559,10.279C12.806,10.09 12.142,9.545 12.026,8.75Z"
android:fillColor="#E6EDFF"/>
<path
android:pathData="M12.019,8.689C11.981,8.345 11.732,8 11.387,8H5.75C3.679,8 2,9.679 2,11.75V17.25C2,19.321 3.679,21 5.75,21H18.25C20.321,21 22,19.321 22,17.25V11.75C22,11.458 21.967,11.175 21.904,10.902C21.811,10.499 21.36,10.324 20.954,10.401C20.581,10.471 20.37,10.904 20.45,11.275C20.483,11.428 20.5,11.587 20.5,11.75V17.25C20.5,18.493 19.493,19.5 18.25,19.5H5.75C4.507,19.5 3.5,18.493 3.5,17.25V11.75C3.5,10.507 4.507,9.5 5.75,9.5H11.456C11.821,9.5 12.058,9.051 12.019,8.689Z"
android:fillColor="#2B55CA"/>
<path
android:pathData="M6,14.5C6,13.672 6.672,13 7.5,13H12.5C13.328,13 14,13.672 14,14.5C14,15.328 13.328,16 12.5,16H7.5C6.672,16 6,15.328 6,14.5Z"
android:fillColor="#8FABF9"/>
<path
android:pathData="M18.665,7.335C18.557,6.4 18.405,5.183 18.31,4.804C18.043,3.744 16.957,3.744 16.69,4.804C16.595,5.183 16.443,6.4 16.335,7.335C15.4,7.443 14.183,7.595 13.804,7.69C12.743,7.957 12.743,9.043 13.804,9.31C14.182,9.405 15.395,9.556 16.329,9.665C16.438,10.636 16.593,11.907 16.69,12.196C17.026,13.196 18.036,13.283 18.31,12.196C18.405,11.818 18.556,10.605 18.665,9.671C19.636,9.562 20.907,9.407 21.197,9.31C22.196,8.974 22.283,7.964 21.197,7.69C20.817,7.595 19.601,7.443 18.665,7.335Z"
android:fillColor="#6B4EBA"
android:fillType="evenOdd"/>
<path
android:pathData="M14,3.5C14,4.328 13.328,5 12.5,5C11.672,5 11,4.328 11,3.5C11,2.672 11.672,2 12.5,2C13.328,2 14,2.672 14,3.5Z"
android:fillColor="#FFCC33"/>
<path
android:pathData="M10,5C10,5.552 9.552,6 9,6C8.448,6 8,5.552 8,5C8,4.448 8.448,4 9,4C9.552,4 10,4.448 10,5Z"
android:fillColor="#FFD7CC"/>
</vector>
20 changes: 20 additions & 0 deletions autofill/autofill-impl/src/main/res/drawable/ic_lock_color_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,8C6,4.686 8.686,2 12,2C15.314,2 18,4.686 18,8V13C18,16.314 15.314,19 12,19C8.686,19 6,16.314 6,13V8ZM12,3.333C9.423,3.333 7.333,5.423 7.333,8V13C7.333,15.577 9.423,17.667 12,17.667C14.577,17.667 16.667,15.577 16.667,13V8C16.667,5.423 14.577,3.333 12,3.333Z"
android:fillColor="#888888"
android:fillType="evenOdd"/>
<path
android:pathData="M3.667,12.667C3.667,12.114 4.115,11.667 4.667,11.667H19.334C19.886,11.667 20.334,12.114 20.334,12.667V20.333C20.334,20.886 19.886,21.333 19.334,21.333H4.667C4.115,21.333 3.667,20.886 3.667,20.333V12.667Z"
android:fillColor="#F9BE1A"/>
<path
android:pathData="M3.667,11.667H12V21.333H3.667V11.667Z"
android:fillColor="#FFDE7A"/>
<path
android:pathData="M3,12.667C3,11.746 3.746,11 4.667,11H19.333C20.254,11 21,11.746 21,12.667V20.333C21,21.254 20.254,22 19.333,22H4.667C3.746,22 3,21.254 3,20.333V12.667ZM4.667,12.333C4.483,12.333 4.333,12.483 4.333,12.667V20.333C4.333,20.517 4.483,20.667 4.667,20.667H19.333C19.517,20.667 19.667,20.517 19.667,20.333V12.667C19.667,12.483 19.517,12.333 19.333,12.333H4.667Z"
android:fillColor="#C18010"
android:fillType="evenOdd"/>
</vector>
Loading

0 comments on commit d6de23b

Please sign in to comment.