Skip to content

Commit

Permalink
UI for the showing/hiding/dismissing/accessing the survey
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed May 3, 2024
1 parent f0eb63f commit 8a642a7
Show file tree
Hide file tree
Showing 14 changed files with 602 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
"m_autofill_device_capability_secure_storage_unavailable_and_device_auth_disabled",
),
AUTOFILL_DEVICE_CAPABILITY_UNKNOWN_ERROR("m_autofill_device_capability_unknown"),

AUTOFILL_SURVEY_AVAILABLE_PROMPT_DISPLAYED("m_autofill_management_screen_visit_survey_available"),
}

@ContributesMultibinding(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_CONFIRMED
Expand Down Expand Up @@ -72,6 +73,8 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsVie
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.PromptUserToAuthenticateMassDeletion
import com.duckduckgo.autofill.impl.ui.credential.management.neversaved.NeverSavedSitesViewState
import com.duckduckgo.autofill.impl.ui.credential.management.searching.CredentialListFilter
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey.SurveyDetails
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.duckaddress.DuckAddressIdentifier
import com.duckduckgo.autofill.impl.ui.credential.repository.DuckAddressStatusRepository
import com.duckduckgo.autofill.impl.ui.credential.repository.DuckAddressStatusRepository.ActivationStatusResult
Expand Down Expand Up @@ -109,6 +112,7 @@ class AutofillSettingsViewModel @Inject constructor(
private val syncEngine: SyncEngine,
private val neverSavedSiteRepository: NeverSavedSiteRepository,
private val capabilityChecker: InternalAutofillCapabilityChecker,
private val autofillSurvey: AutofillSurvey,
) : ViewModel() {

private val _viewState = MutableStateFlow(ViewState())
Expand Down Expand Up @@ -244,6 +248,32 @@ class AutofillSettingsViewModel @Inject constructor(
fun onViewStarted() {
viewModelScope.launch(dispatchers.io()) {
syncEngine.triggerSync(FEATURE_READ)
showSurveyIfAvailable()
}
}

private fun showSurveyIfAvailable() {
viewModelScope.launch(dispatchers.io()) {
val survey = autofillSurvey.firstUnusedSurvey()
_viewState.value = _viewState.value.copy(survey = survey)

if (survey != null) {
pixel.fire(AutofillPixelNames.AUTOFILL_SURVEY_AVAILABLE_PROMPT_DISPLAYED)
}
}
}

fun onSurveyShown(surveyId: String) {
viewModelScope.launch(dispatchers.io()) {
_viewState.value = _viewState.value.copy(survey = null)
autofillSurvey.recordSurveyAsUsed(surveyId)
}
}

fun onSurveyPromptDismissed(surveyId: String) {
viewModelScope.launch(dispatchers.io()) {
_viewState.value = _viewState.value.copy(survey = null)
autofillSurvey.recordSurveyAsUsed(surveyId)
}
}

Expand Down Expand Up @@ -647,6 +677,7 @@ class AutofillSettingsViewModel @Inject constructor(
val credentialMode: CredentialMode? = null,
val credentialSearchQuery: String = "",
val webViewCompatible: Boolean = true,
val survey: SurveyDetails? = null,
)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2024 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.
*/

package com.duckduckgo.autofill.impl.ui.credential.management.survey

import androidx.core.net.toUri
import com.duckduckgo.app.statistics.store.StatisticsDataStore
import com.duckduckgo.app.usage.app.AppDaysUsedRepository
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.autofill.impl.configuration.integration.JavascriptCommunicationSupport
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey.SurveyDetails
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.IN_APP
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_LOTS
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_MANY
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_NONE
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_SOME
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext

interface AutofillSurvey {
suspend fun firstUnusedSurvey(): SurveyDetails?
suspend fun recordSurveyAsUsed(id: String)

data class SurveyDetails(
val id: String,
val url: String,
)
}

@ContributesBinding(AppScope::class)
class AutofillSurveyImpl @Inject constructor(
private val statisticsStore: StatisticsDataStore,
private val userBrowserProperties: UserBrowserProperties,
private val appBuildConfig: AppBuildConfig,
private val appDaysUsedRepository: AppDaysUsedRepository,
private val dispatchers: DispatcherProvider,
private val autofillSurveyStore: AutofillSurveyStore,
private val javascriptCommunicationSupport: JavascriptCommunicationSupport,
private val internalAutofillStore: InternalAutofillStore,
) : AutofillSurvey {

override suspend fun firstUnusedSurvey(): SurveyDetails? {
if (!canShowSurvey()) return null
val survey = availableSurveys.firstOrNull { !surveyTakenPreviously(it.id) } ?: return null
return survey.copy(url = survey.url.addSurveyParameters())
}

private fun canShowSurvey(): Boolean {
return deviceSetToEnglish() && javascriptCommunicationSupport.supportsModernIntegration()
}

override suspend fun recordSurveyAsUsed(id: String) {
autofillSurveyStore.recordSurveyWasShown(id)
}

private fun deviceSetToEnglish(): Boolean {
return appBuildConfig.deviceLocale.language == Locale("en").language
}

private suspend fun surveyTakenPreviously(surveyId: String): Boolean {
return autofillSurveyStore.hasSurveyBeenTaken(surveyId)
}

private suspend fun String.addSurveyParameters(): String {
return withContext(dispatchers.io()) {
val urlBuilder = toUri()
.buildUpon()
.appendQueryParameter(SurveyParams.ATB, statisticsStore.atb?.version ?: "")
.appendQueryParameter(SurveyParams.ATB_VARIANT, statisticsStore.variant)
.appendQueryParameter(SurveyParams.DAYS_INSTALLED, "${userBrowserProperties.daysSinceInstalled()}")
.appendQueryParameter(SurveyParams.ANDROID_VERSION, "${appBuildConfig.sdkInt}")
.appendQueryParameter(SurveyParams.APP_VERSION, appBuildConfig.versionName)
.appendQueryParameter(SurveyParams.MANUFACTURER, appBuildConfig.manufacturer)
.appendQueryParameter(SurveyParams.MODEL, appBuildConfig.model)
.appendQueryParameter(SurveyParams.SOURCE, IN_APP)
.appendQueryParameter(SurveyParams.LAST_ACTIVE_DATE, appDaysUsedRepository.getLastActiveDay())
.appendQueryParameter(SurveyParams.NUMBER_PASSWORDS, bucketSavedPasswords(internalAutofillStore.getCredentialCount().firstOrNull()))

urlBuilder.build().toString()
}
}

private fun bucketSavedPasswords(passwordsSaved: Int?): String {
return when {
passwordsSaved == null -> NUMBER_PASSWORD_BUCKET_NONE
passwordsSaved < 3 -> NUMBER_PASSWORD_BUCKET_NONE
passwordsSaved < 10 -> NUMBER_PASSWORD_BUCKET_SOME
passwordsSaved < 50 -> NUMBER_PASSWORD_BUCKET_MANY
else -> NUMBER_PASSWORD_BUCKET_LOTS
}
}

companion object {
private val availableSurveys = listOf(
SurveyDetails(
id = "autofill-2024-04-26",
url = "https://selfserve.decipherinc.com/survey/selfserve/32ab/240308",
),
)

private object SurveyParams {
const val ATB = "atb"
const val ATB_VARIANT = "var"
const val DAYS_INSTALLED = "delta"
const val ANDROID_VERSION = "av"
const val APP_VERSION = "ddgv"
const val MANUFACTURER = "man"
const val MODEL = "mo"
const val LAST_ACTIVE_DATE = "da"
const val SOURCE = "src"
const val IN_APP = "in_app"
const val NUMBER_PASSWORDS = "saved_passwords"
const val NUMBER_PASSWORD_BUCKET_NONE = "none"
const val NUMBER_PASSWORD_BUCKET_SOME = "some"
const val NUMBER_PASSWORD_BUCKET_MANY = "many"
const val NUMBER_PASSWORD_BUCKET_LOTS = "lots"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 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.
*/

package com.duckduckgo.autofill.impl.ui.credential.management.survey

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.withContext

interface AutofillSurveyStore {
suspend fun hasSurveyBeenTaken(id: String): Boolean
suspend fun recordSurveyWasShown(id: String)
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class AutofillSurveyStoreImpl @Inject constructor(
private val context: Context,
private val dispatchers: DispatcherProvider,
) : AutofillSurveyStore {

private val prefs: SharedPreferences by lazy {
context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
}

override suspend fun hasSurveyBeenTaken(id: String): Boolean {
return withContext(dispatchers.io()) {
val previousSurveys = prefs.getStringSet(SURVEY_IDS, mutableSetOf()) ?: mutableSetOf()
previousSurveys.contains(id)
}
}

override suspend fun recordSurveyWasShown(id: String) {
withContext(dispatchers.io()) {
val currentValue = prefs.getStringSet(SURVEY_IDS, mutableSetOf()) ?: mutableSetOf()
val newValue = currentValue.toMutableSet()
newValue.add(id)
prefs.edit {
putStringSet(SURVEY_IDS, newValue)
}
}
}

companion object {
private const val PREFS_FILE_NAME = "autofill_survey_store"
private const val SURVEY_IDS = "survey_ids"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.favicon.FaviconManager
import com.duckduckgo.app.tabs.BrowserNav
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.FragmentAutofillManagementListModeBinding
Expand All @@ -54,7 +55,9 @@ import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialG
import com.duckduckgo.autofill.impl.ui.credential.management.sorting.InitialExtractor
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilder
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionMatcher
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey.SurveyDetails
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.view.MessageCta.Message
import com.duckduckgo.common.ui.view.SearchBar
import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder
import com.duckduckgo.common.ui.view.gone
Expand All @@ -73,6 +76,9 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
@Inject
lateinit var faviconManager: FaviconManager

@Inject
lateinit var browserNav: BrowserNav

@Inject
lateinit var viewModelFactory: FragmentViewModelFactory

Expand Down Expand Up @@ -226,6 +232,12 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
} else {
binding.webViewUnsupportedWarningPanel.show()
}

if (state.survey == null) {
hideSurvey()
} else {
showSurvey(state.survey)
}
}
}
}
Expand Down Expand Up @@ -262,6 +274,30 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
viewModel.commandProcessed(command)
}

private fun hideSurvey() {
binding.autofillSurveyMessage.gone()
}

private fun showSurvey(survey: SurveyDetails) {
with(binding.autofillSurveyMessage) {
setMessage(
Message(
title = getString(R.string.autofillManagementSurveyPromptTitle),
subtitle = getString(R.string.autofillManagementSurveyPromptMessage),
action = getString(R.string.autofillManagementSurveyPromptAcceptButtonText),
),
)
onPrimaryActionClicked {
startActivity(browserNav.openInNewTab(binding.root.context, survey.url))
viewModel.onSurveyShown(survey.id)
}
onCloseButtonClicked {
viewModel.onSurveyPromptDismissed(survey.id)
}
show()
}
}

private suspend fun credentialsListUpdated(
credentials: List<LoginCredentials>,
credentialSearchQuery: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
app:panelBackground="@drawable/info_panel_alert_background"
app:panelDrawable="@drawable/ic_info_panel_alert" />

<com.duckduckgo.common.ui.view.MessageCta
android:layout_width="0dp"
android:layout_height="wrap_content"
app:contentOrientation="start"
android:visibility="gone"
android:id="@+id/autofillSurveyMessage"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/webViewUnsupportedWarningPanel" />

<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
android:id="@+id/enabledToggle"
android:layout_width="0dp"
Expand All @@ -48,7 +58,7 @@
app:showSwitch="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/webViewUnsupportedWarningPanel"/>
app:layout_constraintTop_toBottomOf="@id/autofillSurveyMessage" />

<com.duckduckgo.common.ui.view.divider.HorizontalDivider
android:id="@+id/topDivider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
<string name="autofillDeleteLoginDialogCancel">Cancel</string>
<string name="autofillDeleteLoginDialogDelete">Delete</string>

<!-- Autofill Management Survey Prompt, shown only to English locale so marked as not translatable -->
<string name="autofillManagementSurveyPromptTitle" translatable="false">Help us improve!</string>
<string name="autofillManagementSurveyPromptMessage" translatable="false">We want to make using passwords in DuckDuckGo better.</string>
<string name="autofillManagementSurveyPromptAcceptButtonText" translatable="false">Take Survey</string>

<string name="autofillManagementUsernameCopied">Username copied</string>
<string name="autofillManagementPasswordCopied">Password copied</string>
Expand Down

0 comments on commit 8a642a7

Please sign in to comment.