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 Apr 26, 2024
1 parent 1ce80b3 commit f72f5e3
Show file tree
Hide file tree
Showing 10 changed files with 442 additions and 1 deletion.
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,116 @@
/*
* 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.ui.credential.management.survey.AutofillSurvey.SurveyDetails
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.IN_APP
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.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,
) : 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())

urlBuilder.build().toString()
}
}

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"
}
}
}
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 password 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 f72f5e3

Please sign in to comment.