Revert "Bug 1952024: composify logins list screen r=android-reviewers,android-l10n-reviewers,sfamisa,delphine" for causing fenix failures @ LoginsMiddlewareTest
This reverts commit 9053b19eaf.
This commit is contained in:
committed by
amarc@mozilla.com
parent
03605a6b93
commit
0a48fc8bca
@@ -58,7 +58,7 @@ private val RippleRadius = 24.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun IconButton(
|
fun IconButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
contentDescription: String?,
|
contentDescription: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickLabel: String? = null,
|
onClickLabel: String? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
@@ -68,11 +68,7 @@ fun IconButton(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.semantics {
|
.semantics { this.contentDescription = contentDescription }
|
||||||
if (contentDescription != null) {
|
|
||||||
this.contentDescription = contentDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.minimumInteractiveComponentSize()
|
.minimumInteractiveComponentSize()
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.lifecycle
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class to be able to change the reference to objects that get replaced when the activity
|
|
||||||
* gets recreated.
|
|
||||||
*
|
|
||||||
* @property context the android [Context]
|
|
||||||
* @property navController A [NavController] for interacting with the androidx navigation library.
|
|
||||||
* @property composeNavController A [NavController] for navigating within the local Composable nav graph.
|
|
||||||
* @property homeActivity so that we can reference openToBrowserAndLoad and browsingMode :(
|
|
||||||
*/
|
|
||||||
class LifecycleHolder(
|
|
||||||
var context: Context,
|
|
||||||
var navController: NavController,
|
|
||||||
var composeNavController: NavController,
|
|
||||||
var homeActivity: HomeActivity,
|
|
||||||
)
|
|
||||||
@@ -19,8 +19,6 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
@@ -29,9 +27,7 @@ import androidx.fragment.app.setFragmentResult
|
|||||||
import androidx.fragment.app.setFragmentResultListener
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import mozilla.components.concept.engine.EngineSession
|
|
||||||
import mozilla.components.concept.menu.MenuController
|
import mozilla.components.concept.menu.MenuController
|
||||||
import mozilla.components.concept.menu.Orientation
|
import mozilla.components.concept.menu.Orientation
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
@@ -44,11 +40,9 @@ import org.mozilla.fenix.biometricauthentication.BiometricAuthenticationManager
|
|||||||
import org.mozilla.fenix.components.StoreProvider
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.databinding.FragmentSavedLoginsBinding
|
import org.mozilla.fenix.databinding.FragmentSavedLoginsBinding
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.hideToolbar
|
|
||||||
import org.mozilla.fenix.ext.registerForActivityResult
|
import org.mozilla.fenix.ext.registerForActivityResult
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.ext.showToolbar
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
import org.mozilla.fenix.lifecycle.LifecycleHolder
|
|
||||||
import org.mozilla.fenix.settings.biometric.DefaultBiometricUtils
|
import org.mozilla.fenix.settings.biometric.DefaultBiometricUtils
|
||||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||||
@@ -59,14 +53,7 @@ import org.mozilla.fenix.settings.logins.controller.LoginsListController
|
|||||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||||
import org.mozilla.fenix.settings.logins.createInitialLoginsListState
|
import org.mozilla.fenix.settings.logins.createInitialLoginsListState
|
||||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||||
import org.mozilla.fenix.settings.logins.ui.DefaultSavedLoginsStorage
|
|
||||||
import org.mozilla.fenix.settings.logins.ui.LoginsMiddleware
|
|
||||||
import org.mozilla.fenix.settings.logins.ui.LoginsSortOrder
|
|
||||||
import org.mozilla.fenix.settings.logins.ui.LoginsState
|
|
||||||
import org.mozilla.fenix.settings.logins.ui.LoginsStore
|
|
||||||
import org.mozilla.fenix.settings.logins.ui.SavedLoginsScreen
|
|
||||||
import org.mozilla.fenix.settings.logins.view.SavedLoginsListView
|
import org.mozilla.fenix.settings.logins.view.SavedLoginsListView
|
||||||
import org.mozilla.fenix.theme.FirefoxTheme
|
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
||||||
@@ -91,9 +78,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (requireContext().settings().enableComposeLogins) { return }
|
|
||||||
|
|
||||||
startForResult = registerForActivityResult {
|
startForResult = registerForActivityResult {
|
||||||
BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt =
|
BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt =
|
||||||
false
|
false
|
||||||
@@ -105,12 +89,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
if (requireContext().settings().enableComposeLogins) {
|
|
||||||
hideToolbar()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt) {
|
if (BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt) {
|
||||||
BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt =
|
BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt =
|
||||||
false
|
false
|
||||||
@@ -141,72 +119,11 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
|||||||
initToolbar()
|
initToolbar()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View? {
|
||||||
if (requireContext().settings().enableComposeLogins) {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
val buildStore = { navController: NavHostController ->
|
|
||||||
val store = StoreProvider.get(this@SavedLoginsFragment) {
|
|
||||||
val lifecycleHolder = LifecycleHolder(
|
|
||||||
context = requireContext(),
|
|
||||||
navController = this@SavedLoginsFragment.findNavController(),
|
|
||||||
composeNavController = navController,
|
|
||||||
homeActivity = (requireActivity() as HomeActivity),
|
|
||||||
)
|
|
||||||
|
|
||||||
LoginsStore(
|
|
||||||
initialState = LoginsState().copy(
|
|
||||||
sortOrder = LoginsSortOrder.fromString(
|
|
||||||
value = requireContext().settings().loginsListSortOrder,
|
|
||||||
default = LoginsSortOrder.Alphabetical,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
middleware = listOf(
|
|
||||||
LoginsMiddleware(
|
|
||||||
loginsStorage = requireContext().components.core.passwordsStorage,
|
|
||||||
getNavController = { lifecycleHolder.composeNavController },
|
|
||||||
exitLogins = { lifecycleHolder.navController.popBackStack() },
|
|
||||||
persistLoginsSortOrder = {
|
|
||||||
DefaultSavedLoginsStorage(requireContext().settings()).savedLoginsSortOrder =
|
|
||||||
it
|
|
||||||
},
|
|
||||||
openTab = { url, openInNewTab ->
|
|
||||||
lifecycleHolder.homeActivity.openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = url,
|
|
||||||
newTab = openInNewTab,
|
|
||||||
from = BrowserDirection.FromSavedLoginsFragment,
|
|
||||||
flags = EngineSession.LoadUrlFlags.select(
|
|
||||||
EngineSession.LoadUrlFlags.ALLOW_JAVASCRIPT_URL,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
lifecycleHolder = lifecycleHolder,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
store.lifecycleHolder?.apply {
|
|
||||||
this.navController = this@SavedLoginsFragment.findNavController()
|
|
||||||
this.composeNavController = navController
|
|
||||||
this.homeActivity = (requireActivity() as HomeActivity)
|
|
||||||
this.context = requireContext()
|
|
||||||
}
|
|
||||||
|
|
||||||
store
|
|
||||||
}
|
|
||||||
setContent {
|
|
||||||
FirefoxTheme {
|
|
||||||
SavedLoginsScreen(buildStore = buildStore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val view = inflater.inflate(R.layout.fragment_saved_logins, container, false)
|
val view = inflater.inflate(R.layout.fragment_saved_logins, container, false)
|
||||||
|
|
||||||
_binding = FragmentSavedLoginsBinding.bind(view)
|
_binding = FragmentSavedLoginsBinding.bind(view)
|
||||||
@@ -252,10 +169,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
if (requireContext().settings().enableComposeLogins) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setFragmentResultListener(LoginDetailFragment.LOGIN_REQUEST_KEY) { _, bundle ->
|
setFragmentResultListener(LoginDetailFragment.LOGIN_REQUEST_KEY) { _, bundle ->
|
||||||
removedLoginGuid = bundle.getString(LoginDetailFragment.LOGIN_BUNDLE_ARGS)
|
removedLoginGuid = bundle.getString(LoginDetailFragment.LOGIN_BUNDLE_ARGS)
|
||||||
deletedGuid.add(removedLoginGuid.toString())
|
deletedGuid.add(removedLoginGuid.toString())
|
||||||
@@ -338,11 +251,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider {
|
|||||||
* If we pause this fragment, we want to pop users back to reauth
|
* If we pause this fragment, we want to pop users back to reauth
|
||||||
*/
|
*/
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
if (requireContext().settings().enableComposeLogins) {
|
|
||||||
super.onPause()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbarChildContainer.removeAllViews()
|
toolbarChildContainer.removeAllViews()
|
||||||
toolbarChildContainer.visibility = View.GONE
|
toolbarChildContainer.visibility = View.GONE
|
||||||
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary()
|
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary()
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import org.mozilla.fenix.utils.Settings
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface to persist the state of the saved logins screen.
|
|
||||||
*/
|
|
||||||
interface SavedLoginsStorage {
|
|
||||||
/**
|
|
||||||
* Indicates the sort order of the saved logins list.
|
|
||||||
*/
|
|
||||||
var savedLoginsSortOrder: LoginsSortOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A default implementation of `SavedLoginsStorage`.
|
|
||||||
*
|
|
||||||
* @property settings The settings object used to persist the saved logins screen state.
|
|
||||||
*/
|
|
||||||
class DefaultSavedLoginsStorage(
|
|
||||||
val settings: Settings,
|
|
||||||
) : SavedLoginsStorage {
|
|
||||||
override var savedLoginsSortOrder
|
|
||||||
get() = LoginsSortOrder.fromString(settings.loginsListSortOrder)
|
|
||||||
set(value) {
|
|
||||||
settings.loginsListSortOrder = value.asString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An item representing a saved login
|
|
||||||
*
|
|
||||||
* @property guid The id of the login.
|
|
||||||
* @property url The site where the login is created.
|
|
||||||
* @property username The username of the login.
|
|
||||||
* @property password The password of the login.
|
|
||||||
* @property timeLastUsed The time in milliseconds when the login was last used.
|
|
||||||
*/
|
|
||||||
data class LoginItem(
|
|
||||||
val guid: String,
|
|
||||||
val url: String,
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
val timeLastUsed: Long = 0L,
|
|
||||||
)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import mozilla.components.lib.state.Action
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions relating to the Logins list screen and its various subscreens.
|
|
||||||
*/
|
|
||||||
internal sealed interface LoginsAction : Action
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Store is initializing.
|
|
||||||
*/
|
|
||||||
internal data object Init : LoginsAction
|
|
||||||
internal data class InitDetails(val guid: String) : LoginsAction
|
|
||||||
internal data class InitEdit(val guid: String) : LoginsAction
|
|
||||||
internal data class InitEditLoaded(
|
|
||||||
val login: LoginItem,
|
|
||||||
) : LoginsAction
|
|
||||||
|
|
||||||
internal data object InitAdd : LoginsAction
|
|
||||||
internal data class InitAddLoaded(
|
|
||||||
val login: LoginItem,
|
|
||||||
) : LoginsAction
|
|
||||||
|
|
||||||
internal data object ViewDisposed : LoginsAction
|
|
||||||
internal data object LoginsListBackClicked : LoginsAction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logins have been loaded from the storage layer.
|
|
||||||
*
|
|
||||||
* @property loginItems The login items loaded, transformed into a displayable type.
|
|
||||||
*/
|
|
||||||
internal data class LoginsLoaded(
|
|
||||||
val loginItems: List<LoginItem>,
|
|
||||||
) : LoginsAction
|
|
||||||
|
|
||||||
internal sealed class LoginsListSortMenuAction : LoginsAction {
|
|
||||||
data object OrderByNameClicked : LoginsListSortMenuAction()
|
|
||||||
data object OrderByLastUsedClicked : LoginsListSortMenuAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DetailLoginMenuAction : LoginsAction {
|
|
||||||
data class EditLoginMenuItemClicked(val item: LoginItem) : DetailLoginMenuAction()
|
|
||||||
data class DeleteLoginMenuItemClicked(val item: LoginItem) : DetailLoginMenuAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class SearchLogins(val searchText: String, val loginItems: List<LoginItem>) :
|
|
||||||
LoginsAction
|
|
||||||
|
|
||||||
internal data class LoginClicked(val item: LoginItem) : LoginsAction
|
|
||||||
internal data object LearnMoreAboutSync : LoginsAction
|
|
||||||
|
|
||||||
internal sealed class EditLoginAction : LoginsAction {
|
|
||||||
data class UsernameChanged(val usernameChanged: String) : EditLoginAction()
|
|
||||||
data class PasswordChanged(val passwordChanged: String) : EditLoginAction()
|
|
||||||
data class PasswordVisible(val visible: Boolean) : EditLoginAction()
|
|
||||||
data object UsernameClearClicked : EditLoginAction()
|
|
||||||
data object PasswordClearClicked : EditLoginAction()
|
|
||||||
data class SaveEditClicked(val login: LoginItem) : EditLoginAction()
|
|
||||||
data object BackEditClicked : EditLoginAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class AddLoginAction : LoginsAction {
|
|
||||||
data class UrlChanged(val urlChanged: String) : AddLoginAction()
|
|
||||||
data class UsernameChanged(val usernameChanged: String) : AddLoginAction()
|
|
||||||
data class PasswordChanged(val passwordChanged: String) : AddLoginAction()
|
|
||||||
data object UrlClearClicked : AddLoginAction()
|
|
||||||
data object UsernameClearClicked : AddLoginAction()
|
|
||||||
data object PasswordClearClicked : AddLoginAction()
|
|
||||||
data object SaveAddClicked : AddLoginAction()
|
|
||||||
data object BackAddClicked : AddLoginAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DetailLoginAction : LoginsAction {
|
|
||||||
data object OptionsMenuClicked : DetailLoginAction()
|
|
||||||
data class GoToSiteClicked(val url: String) : DetailLoginAction()
|
|
||||||
data class CopyUsernameClicked(val username: String) : DetailLoginAction()
|
|
||||||
data class CopyPasswordClicked(val password: String) : DetailLoginAction()
|
|
||||||
data class PasswordVisibleClicked(val visible: Boolean) : DetailLoginAction()
|
|
||||||
data object BackDetailClicked : DetailLoginAction()
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import mozilla.components.concept.storage.LoginsStorage
|
|
||||||
import mozilla.components.lib.state.Middleware
|
|
||||||
import mozilla.components.lib.state.MiddlewareContext
|
|
||||||
import mozilla.components.lib.state.Store
|
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A middleware for handling side-effects in response to [LoginsAction]s.
|
|
||||||
*
|
|
||||||
* @param loginsStorage Storage layer for reading and writing logins.
|
|
||||||
* @param getNavController Fetch the NavController for navigating within the local Composable nav graph.
|
|
||||||
* @param exitLogins Invoked when back is clicked while the navController's backstack is empty.
|
|
||||||
* @param persistLoginsSortOrder Invoked to persist the new sorting order for logins.
|
|
||||||
* @param openTab Invoked when opening a tab when a login url is clicked.
|
|
||||||
* @param ioDispatcher Coroutine dispatcher for IO operations.
|
|
||||||
*/
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
internal class LoginsMiddleware(
|
|
||||||
private val loginsStorage: LoginsStorage,
|
|
||||||
private val getNavController: () -> NavController,
|
|
||||||
private val exitLogins: () -> Unit,
|
|
||||||
private val persistLoginsSortOrder: suspend (LoginsSortOrder) -> Unit,
|
|
||||||
private val openTab: (url: String, openInNewTab: Boolean) -> Unit,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
|
||||||
) : Middleware<LoginsState, LoginsAction> {
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(ioDispatcher)
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "ComplexMethod")
|
|
||||||
override fun invoke(
|
|
||||||
context: MiddlewareContext<LoginsState, LoginsAction>,
|
|
||||||
next: (LoginsAction) -> Unit,
|
|
||||||
action: LoginsAction,
|
|
||||||
) {
|
|
||||||
next(action)
|
|
||||||
|
|
||||||
when (action) {
|
|
||||||
Init -> {
|
|
||||||
context.store.loadLoginsList()
|
|
||||||
}
|
|
||||||
is InitEdit -> scope.launch {
|
|
||||||
Result.runCatching {
|
|
||||||
val login = loginsStorage.get(action.guid)
|
|
||||||
val loginItem = login?.let {
|
|
||||||
LoginItem(
|
|
||||||
guid = it.guid,
|
|
||||||
url = it.formActionOrigin ?: "",
|
|
||||||
username = it.username,
|
|
||||||
password = it.password,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
InitEditLoaded(login = loginItem!!)
|
|
||||||
}.getOrNull()?.also {
|
|
||||||
context.store.dispatch(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is InitAdd -> {
|
|
||||||
getNavController().navigate(LoginsDestinations.ADD_LOGIN)
|
|
||||||
}
|
|
||||||
is LoginClicked -> {
|
|
||||||
getNavController().navigate(LoginsDestinations.LOGIN_DETAILS)
|
|
||||||
}
|
|
||||||
is SearchLogins -> {
|
|
||||||
context.store.loadLoginsList()
|
|
||||||
}
|
|
||||||
is LoginsListBackClicked -> exitLogins()
|
|
||||||
is DetailLoginMenuAction.EditLoginMenuItemClicked -> getNavController().navigate(
|
|
||||||
LoginsDestinations.EDIT_LOGIN,
|
|
||||||
)
|
|
||||||
is DetailLoginMenuAction.DeleteLoginMenuItemClicked -> {
|
|
||||||
scope.launch {
|
|
||||||
loginsStorage.delete(action.item.guid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is LoginsListSortMenuAction -> scope.launch {
|
|
||||||
persistLoginsSortOrder(context.store.state.sortOrder)
|
|
||||||
}
|
|
||||||
is LearnMoreAboutSync -> {
|
|
||||||
openTab(
|
|
||||||
SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.SYNC_SETUP),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is DetailLoginAction.GoToSiteClicked -> {
|
|
||||||
openTab(action.url, true)
|
|
||||||
}
|
|
||||||
is InitEditLoaded,
|
|
||||||
is EditLoginAction.UsernameChanged,
|
|
||||||
is AddLoginAction.BackAddClicked,
|
|
||||||
is DetailLoginAction.BackDetailClicked,
|
|
||||||
is EditLoginAction.BackEditClicked,
|
|
||||||
is DetailLoginAction.CopyPasswordClicked,
|
|
||||||
is DetailLoginAction.CopyUsernameClicked,
|
|
||||||
is InitAddLoaded,
|
|
||||||
is InitDetails,
|
|
||||||
is LoginsLoaded,
|
|
||||||
is DetailLoginAction.OptionsMenuClicked,
|
|
||||||
is EditLoginAction.PasswordChanged,
|
|
||||||
is AddLoginAction.PasswordChanged,
|
|
||||||
is EditLoginAction.PasswordClearClicked,
|
|
||||||
is AddLoginAction.PasswordClearClicked,
|
|
||||||
is EditLoginAction.PasswordVisible,
|
|
||||||
is DetailLoginAction.PasswordVisibleClicked,
|
|
||||||
is AddLoginAction.SaveAddClicked,
|
|
||||||
is EditLoginAction.SaveEditClicked,
|
|
||||||
is AddLoginAction.UrlChanged,
|
|
||||||
is AddLoginAction.UrlClearClicked,
|
|
||||||
is AddLoginAction.UsernameChanged,
|
|
||||||
is EditLoginAction.UsernameClearClicked,
|
|
||||||
is AddLoginAction.UsernameClearClicked,
|
|
||||||
is ViewDisposed,
|
|
||||||
-> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Store<LoginsState, LoginsAction>.loadLoginsList() = scope.launch {
|
|
||||||
val loginItems = arrayListOf<LoginItem>()
|
|
||||||
val items = loginsStorage.list()
|
|
||||||
items.forEach { login ->
|
|
||||||
loginItems.add(
|
|
||||||
LoginItem(
|
|
||||||
guid = login.guid,
|
|
||||||
url = login.origin,
|
|
||||||
username = login.username,
|
|
||||||
password = login.password,
|
|
||||||
timeLastUsed = login.timeLastUsed,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(LoginsLoaded(loginItems))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for reducing a new logins state based on the received action.
|
|
||||||
*/
|
|
||||||
internal fun loginsReducer(state: LoginsState, action: LoginsAction) = when (action) {
|
|
||||||
is InitEditLoaded -> state.copy(
|
|
||||||
loginsEditLoginState = LoginsEditLoginState(
|
|
||||||
login = action.login,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
is LoginsLoaded -> {
|
|
||||||
state.handleLoginsLoadedAction(action)
|
|
||||||
}
|
|
||||||
is LoginsListSortMenuAction -> {
|
|
||||||
state.handleSortMenuAction(action)
|
|
||||||
}
|
|
||||||
is SearchLogins -> {
|
|
||||||
state.handleSearchLogins(action)
|
|
||||||
}
|
|
||||||
is LoginClicked -> if (state.loginItems.isNotEmpty()) {
|
|
||||||
state.toggleSelectionOf(action.item)
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
}
|
|
||||||
is EditLoginAction -> state.loginsEditLoginState?.let {
|
|
||||||
state.copy(loginsEditLoginState = it.handleEditLoginAction(action))
|
|
||||||
} ?: state
|
|
||||||
is AddLoginAction -> state.loginsAddLoginState?.let {
|
|
||||||
state.copy(loginsAddLoginState = handleAddLoginAction(action))
|
|
||||||
} ?: state
|
|
||||||
is DetailLoginAction -> state.loginsLoginDetailState?.let {
|
|
||||||
state.copy(loginsLoginDetailState = handleDetailLoginAction(action))
|
|
||||||
} ?: state
|
|
||||||
is DetailLoginMenuAction -> state
|
|
||||||
is LoginsListBackClicked -> state.respondToLoginsListBackClick()
|
|
||||||
ViewDisposed,
|
|
||||||
is InitEdit, Init, InitAdd, LearnMoreAboutSync, is InitDetails, is InitAddLoaded,
|
|
||||||
-> state
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LoginsState.handleSearchLogins(action: SearchLogins): LoginsState = copy(
|
|
||||||
searchText = action.searchText,
|
|
||||||
loginItems = action.loginItems.filter {
|
|
||||||
it.url.contains(
|
|
||||||
action.searchText,
|
|
||||||
ignoreCase = true,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun LoginsState.handleLoginsLoadedAction(action: LoginsLoaded): LoginsState =
|
|
||||||
copy(
|
|
||||||
loginItems = if (searchText.isNullOrEmpty()) {
|
|
||||||
action.loginItems.sortedWith(sortOrder.comparator)
|
|
||||||
} else {
|
|
||||||
action.loginItems.sortedWith(sortOrder.comparator)
|
|
||||||
.filter { it.url.contains(searchText, ignoreCase = true) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun LoginsState.toggleSelectionOf(item: LoginItem): LoginsState =
|
|
||||||
if (loginItems.any { it.guid == item.guid }) {
|
|
||||||
copy(loginItems = loginItems - item)
|
|
||||||
} else {
|
|
||||||
copy(loginItems = loginItems + item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LoginsState.respondToLoginsListBackClick(): LoginsState = when {
|
|
||||||
loginsListState != null -> copy(loginsListState = null)
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LoginsState.handleSortMenuAction(action: LoginsListSortMenuAction): LoginsState =
|
|
||||||
when (action) {
|
|
||||||
LoginsListSortMenuAction.OrderByLastUsedClicked -> copy(sortOrder = LoginsSortOrder.LastUsed)
|
|
||||||
LoginsListSortMenuAction.OrderByNameClicked -> copy(sortOrder = LoginsSortOrder.Alphabetical)
|
|
||||||
}.let {
|
|
||||||
it.copy(
|
|
||||||
loginItems = it.loginItems.sortedWith(it.sortOrder.comparator),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LoginsEditLoginState.handleEditLoginAction(action: EditLoginAction): LoginsEditLoginState? =
|
|
||||||
when (action) {
|
|
||||||
is EditLoginAction.UsernameChanged -> copy(
|
|
||||||
login = login.copy(password = action.usernameChanged),
|
|
||||||
)
|
|
||||||
is EditLoginAction.PasswordChanged -> copy(
|
|
||||||
login = login.copy(password = action.passwordChanged),
|
|
||||||
)
|
|
||||||
is EditLoginAction.UsernameClearClicked,
|
|
||||||
is EditLoginAction.PasswordClearClicked,
|
|
||||||
is EditLoginAction.PasswordVisible,
|
|
||||||
is EditLoginAction.SaveEditClicked,
|
|
||||||
is EditLoginAction.BackEditClicked,
|
|
||||||
-> null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAddLoginAction(action: AddLoginAction): LoginsAddLoginState? =
|
|
||||||
when (action) {
|
|
||||||
is AddLoginAction.UrlChanged,
|
|
||||||
is AddLoginAction.PasswordChanged,
|
|
||||||
is AddLoginAction.UsernameChanged,
|
|
||||||
is AddLoginAction.UrlClearClicked,
|
|
||||||
is AddLoginAction.PasswordClearClicked,
|
|
||||||
is AddLoginAction.UsernameClearClicked,
|
|
||||||
is AddLoginAction.SaveAddClicked,
|
|
||||||
is AddLoginAction.BackAddClicked,
|
|
||||||
-> null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDetailLoginAction(action: DetailLoginAction): LoginsLoginDetailState? =
|
|
||||||
when (action) {
|
|
||||||
is DetailLoginAction.OptionsMenuClicked,
|
|
||||||
is DetailLoginAction.GoToSiteClicked,
|
|
||||||
is DetailLoginAction.CopyUsernameClicked,
|
|
||||||
is DetailLoginAction.CopyPasswordClicked,
|
|
||||||
is DetailLoginAction.PasswordVisibleClicked,
|
|
||||||
is DetailLoginAction.BackDetailClicked,
|
|
||||||
-> null
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import mozilla.components.lib.state.State
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the state of the Logins list screen and its various subscreens.
|
|
||||||
*
|
|
||||||
* @property loginItems Login items to be displayed in the current list screen.
|
|
||||||
* @property searchText The text to filter login items.
|
|
||||||
* @property sortOrder The order to display the login items.
|
|
||||||
* @property biometricAuthenticationDialogState State representing the biometric authentication state.
|
|
||||||
* @property loginsListState State representing the list login subscreen, if visible.
|
|
||||||
* @property loginsAddLoginState State representing the add login subscreen, if visible.
|
|
||||||
* @property loginsEditLoginState State representing the edit login subscreen, if visible.
|
|
||||||
* @property loginsLoginDetailState State representing the login detail subscreen, if visible.
|
|
||||||
* @property loginsDeletionDialogState State representing the deletion dialog state.
|
|
||||||
*/
|
|
||||||
internal data class LoginsState(
|
|
||||||
val loginItems: List<LoginItem> = listOf(),
|
|
||||||
val searchText: String? = null,
|
|
||||||
val sortOrder: LoginsSortOrder = LoginsSortOrder.default,
|
|
||||||
val biometricAuthenticationDialogState: BiometricAuthenticationDialogState? =
|
|
||||||
BiometricAuthenticationDialogState.None,
|
|
||||||
val loginsListState: LoginsListState? = null,
|
|
||||||
val loginsAddLoginState: LoginsAddLoginState? = null,
|
|
||||||
val loginsEditLoginState: LoginsEditLoginState? = null,
|
|
||||||
val loginsLoginDetailState: LoginsLoginDetailState? = null,
|
|
||||||
val loginsDeletionDialogState: DeletionDialogState? = null,
|
|
||||||
) : State
|
|
||||||
|
|
||||||
internal sealed class BiometricAuthenticationDialogState {
|
|
||||||
data object None : BiometricAuthenticationDialogState()
|
|
||||||
data object Authorized : BiometricAuthenticationDialogState()
|
|
||||||
data object NonAuthorized : BiometricAuthenticationDialogState()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DeletionDialogState {
|
|
||||||
data object None : DeletionDialogState()
|
|
||||||
data class Presenting(
|
|
||||||
val guidToDelete: String,
|
|
||||||
) : DeletionDialogState()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class LoginsListState(
|
|
||||||
val logins: List<LoginItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal data class LoginsEditLoginState(
|
|
||||||
val login: LoginItem,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal sealed class LoginsAddLoginState {
|
|
||||||
data object None : LoginsAddLoginState()
|
|
||||||
data class Presenting(val login: LoginItem) : LoginsAddLoginState()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class LoginsLoginDetailState(
|
|
||||||
val login: LoginItem,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the order of the Logins list items.
|
|
||||||
*/
|
|
||||||
sealed class LoginsSortOrder {
|
|
||||||
abstract val asString: String
|
|
||||||
abstract val comparator: Comparator<LoginItem>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the ordering of the logins list when sorted alphabetically.
|
|
||||||
*/
|
|
||||||
data object Alphabetical : LoginsSortOrder() {
|
|
||||||
override val asString: String
|
|
||||||
get() = "alphabetical"
|
|
||||||
|
|
||||||
override val comparator: Comparator<LoginItem>
|
|
||||||
get() = compareBy { it.url }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the ordering of the logins list when sorted by the last used date.
|
|
||||||
*/
|
|
||||||
data object LastUsed : LoginsSortOrder() {
|
|
||||||
override val asString: String
|
|
||||||
get() = "last-used"
|
|
||||||
|
|
||||||
override val comparator: Comparator<LoginItem>
|
|
||||||
get() = compareByDescending { it.timeLastUsed }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the [LoginsSortOrder] object.
|
|
||||||
*/
|
|
||||||
companion object {
|
|
||||||
val default: LoginsSortOrder
|
|
||||||
get() = Alphabetical
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a string into a [LoginsSortOrder] object.
|
|
||||||
*/
|
|
||||||
fun fromString(value: String, default: LoginsSortOrder = Alphabetical): LoginsSortOrder {
|
|
||||||
return when (value) {
|
|
||||||
"alphabetical" -> Alphabetical
|
|
||||||
"last-used" -> LastUsed
|
|
||||||
else -> default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import mozilla.components.lib.state.Middleware
|
|
||||||
import mozilla.components.lib.state.Reducer
|
|
||||||
import mozilla.components.lib.state.UiStore
|
|
||||||
import org.mozilla.fenix.lifecycle.LifecycleHolder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Store for handling [LoginsState] and dispatching [LoginsAction].
|
|
||||||
*
|
|
||||||
* @param initialState The initial state for the Store.
|
|
||||||
* @param reducer Reducer to handle state updates based on dispatched actions.
|
|
||||||
* @param middleware Middleware to handle side-effects in response to dispatched actions.
|
|
||||||
* @property lifecycleHolder a hack to box the references to objects that get recreated with the activity.
|
|
||||||
* @param loginToLoad The guid of a login to load when landing on the edit/details screen.
|
|
||||||
*/
|
|
||||||
internal class LoginsStore(
|
|
||||||
initialState: LoginsState = LoginsState(),
|
|
||||||
reducer: Reducer<LoginsState, LoginsAction> = ::loginsReducer,
|
|
||||||
middleware: List<Middleware<LoginsState, LoginsAction>> = listOf(),
|
|
||||||
val lifecycleHolder: LifecycleHolder? = null,
|
|
||||||
loginToLoad: String? = null,
|
|
||||||
) : UiStore<LoginsState, LoginsAction>(
|
|
||||||
initialState = initialState,
|
|
||||||
reducer = reducer,
|
|
||||||
middleware = middleware,
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
val action = loginToLoad?.let { InitEdit(loginToLoad) } ?: Init
|
|
||||||
dispatch(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
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.semantics.CollectionInfo
|
|
||||||
import androidx.compose.ui.semantics.collectionInfo
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
|
|
||||||
import mozilla.components.compose.base.button.IconButton
|
|
||||||
import mozilla.components.compose.base.menu.DropdownMenu
|
|
||||||
import mozilla.components.compose.base.menu.MenuItem
|
|
||||||
import mozilla.components.compose.base.textfield.TextField
|
|
||||||
import mozilla.components.compose.base.textfield.TextFieldColors
|
|
||||||
import mozilla.components.lib.state.ext.observeAsState
|
|
||||||
import mozilla.components.support.ktx.kotlin.trimmed
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.compose.LinkText
|
|
||||||
import org.mozilla.fenix.compose.LinkTextState
|
|
||||||
import org.mozilla.fenix.compose.list.IconListItem
|
|
||||||
import org.mozilla.fenix.compose.list.SelectableFaviconListItem
|
|
||||||
import org.mozilla.fenix.theme.FirefoxTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The UI host for the Saved Logins list screen and related sub screens.
|
|
||||||
*
|
|
||||||
* @param buildStore A builder function to construct a [LoginsStore] using the NavController that's local
|
|
||||||
* to the nav graph for the Logins view hierarchy.
|
|
||||||
* @param startDestination the screen on which to initialize [SavedLoginsScreen] with.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun SavedLoginsScreen(
|
|
||||||
buildStore: (NavHostController) -> LoginsStore,
|
|
||||||
startDestination: String = LoginsDestinations.LIST,
|
|
||||||
) {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
val store = buildStore(navController)
|
|
||||||
|
|
||||||
DisposableEffect(LocalLifecycleOwner.current) {
|
|
||||||
onDispose {
|
|
||||||
store.dispatch(ViewDisposed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(
|
|
||||||
navController = navController,
|
|
||||||
startDestination = startDestination,
|
|
||||||
) {
|
|
||||||
composable(route = LoginsDestinations.LIST) {
|
|
||||||
BackHandler { store.dispatch(LoginsListBackClicked) }
|
|
||||||
LoginsList(store = store)
|
|
||||||
}
|
|
||||||
composable(route = LoginsDestinations.ADD_LOGIN) {
|
|
||||||
BackHandler { store.dispatch(AddLoginAction.BackAddClicked) }
|
|
||||||
}
|
|
||||||
composable(route = LoginsDestinations.EDIT_LOGIN) {
|
|
||||||
BackHandler { store.dispatch(EditLoginAction.BackEditClicked) }
|
|
||||||
}
|
|
||||||
composable(route = LoginsDestinations.LOGIN_DETAILS) {
|
|
||||||
BackHandler { store.dispatch(DetailLoginAction.BackDetailClicked) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object LoginsDestinations {
|
|
||||||
const val LIST = "list"
|
|
||||||
const val ADD_LOGIN = "add login"
|
|
||||||
const val EDIT_LOGIN = "edit login"
|
|
||||||
const val LOGIN_DETAILS = "login details"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoginsList(store: LoginsStore) {
|
|
||||||
val state by store.observeAsState(store.state) { it }
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
LoginsListTopBar(
|
|
||||||
store = store,
|
|
||||||
text = state.searchText ?: "",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundColor = FirefoxTheme.colors.layer1,
|
|
||||||
) { paddingValues ->
|
|
||||||
|
|
||||||
if (state.searchText.isNullOrEmpty() && state.loginItems.isEmpty()) {
|
|
||||||
EmptyList(dispatcher = store::dispatch)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
.semantics {
|
|
||||||
collectionInfo =
|
|
||||||
CollectionInfo(rowCount = state.loginItems.size, columnCount = 1)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
itemsIndexed(state.loginItems) { _, item ->
|
|
||||||
|
|
||||||
SelectableFaviconListItem(
|
|
||||||
label = item.url.trimmed(),
|
|
||||||
url = item.url,
|
|
||||||
isSelected = false,
|
|
||||||
onClick = { store.dispatch(LoginClicked(item)) },
|
|
||||||
description = item.username.trimmed(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
AddPasswordItem(
|
|
||||||
onAddPasswordClicked = { store.dispatch(InitAdd) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AddPasswordItem(
|
|
||||||
onAddPasswordClicked: () -> Unit,
|
|
||||||
) {
|
|
||||||
IconListItem(
|
|
||||||
label = stringResource(R.string.preferences_logins_add_login_2),
|
|
||||||
beforeIconPainter = painterResource(R.drawable.ic_new),
|
|
||||||
onClick = { onAddPasswordClicked() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
private fun EmptyList(
|
|
||||||
dispatcher: (LoginsAction) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.TopStart,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = modifier.padding(16.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = String.format(
|
|
||||||
stringResource(R.string.preferences_passwords_saved_logins_description_empty_text_2),
|
|
||||||
stringResource(R.string.app_name),
|
|
||||||
),
|
|
||||||
style = FirefoxTheme.typography.body2,
|
|
||||||
color = FirefoxTheme.colors.textPrimary,
|
|
||||||
)
|
|
||||||
|
|
||||||
LinkText(
|
|
||||||
text = stringResource(R.string.preferences_passwords_saved_logins_description_empty_learn_more_link_2),
|
|
||||||
linkTextStates = listOf(
|
|
||||||
LinkTextState(
|
|
||||||
text = stringResource(R.string.preferences_passwords_saved_logins_description_empty_learn_more_link_2),
|
|
||||||
url = "",
|
|
||||||
onClick = { dispatcher(LearnMoreAboutSync) },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style = FirefoxTheme.typography.body2.copy(
|
|
||||||
color = FirefoxTheme.colors.textPrimary,
|
|
||||||
),
|
|
||||||
linkTextColor = FirefoxTheme.colors.textPrimary,
|
|
||||||
linkTextDecoration = TextDecoration.Underline,
|
|
||||||
)
|
|
||||||
|
|
||||||
AddPasswordItem(
|
|
||||||
onAddPasswordClicked = { dispatcher(InitAdd) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
private fun LoginsListTopBar(
|
|
||||||
store: LoginsStore,
|
|
||||||
text: String,
|
|
||||||
) {
|
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
|
||||||
var searchActive by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val iconColor = FirefoxTheme.colors.iconPrimary
|
|
||||||
|
|
||||||
Box {
|
|
||||||
TopAppBar(
|
|
||||||
backgroundColor = FirefoxTheme.colors.layer1,
|
|
||||||
title = {
|
|
||||||
if (!searchActive) {
|
|
||||||
Text(
|
|
||||||
color = FirefoxTheme.colors.textPrimary,
|
|
||||||
style = FirefoxTheme.typography.headline6,
|
|
||||||
text = stringResource(R.string.preferences_passwords_saved_logins_2),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (!searchActive) {
|
|
||||||
store.dispatch(LoginsListBackClicked)
|
|
||||||
} else {
|
|
||||||
searchActive = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentDescription = null,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.mozac_ic_back_24),
|
|
||||||
contentDescription = stringResource(R.string.logins_navigate_back_button_content_description),
|
|
||||||
tint = iconColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (!searchActive) {
|
|
||||||
Box {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
showMenu = true
|
|
||||||
},
|
|
||||||
painter = if (showMenu) {
|
|
||||||
painterResource(R.drawable.ic_chevron_up)
|
|
||||||
} else {
|
|
||||||
painterResource(R.drawable.ic_chevron_down)
|
|
||||||
},
|
|
||||||
contentDescription = stringResource(
|
|
||||||
R.string.saved_logins_menu_dropdown_chevron_icon_content_description_2,
|
|
||||||
),
|
|
||||||
tint = iconColor,
|
|
||||||
)
|
|
||||||
|
|
||||||
LoginListSortMenu(
|
|
||||||
showMenu = showMenu,
|
|
||||||
onDismissRequest = {
|
|
||||||
showMenu = false
|
|
||||||
},
|
|
||||||
store = store,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { searchActive = true }, contentDescription = null) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_search),
|
|
||||||
contentDescription = stringResource(R.string.preferences_passwords_saved_logins_search_2),
|
|
||||||
tint = iconColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Box {
|
|
||||||
TextField(
|
|
||||||
value = text,
|
|
||||||
placeholder = stringResource(R.string.preferences_passwords_saved_logins_search_2),
|
|
||||||
onValueChange = {
|
|
||||||
store.dispatch(SearchLogins(it, store.state.loginItems))
|
|
||||||
},
|
|
||||||
errorText = "",
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = FirefoxTheme.colors.layer1)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
trailingIcons = {
|
|
||||||
if (text.isNotBlank()) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
store.dispatch(
|
|
||||||
SearchLogins(
|
|
||||||
"",
|
|
||||||
store.state.loginItems,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentDescription = null,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.mozac_ic_cross_24),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = iconColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TextFieldColors.default(
|
|
||||||
placeholderColor = FirefoxTheme.colors.textPrimary,
|
|
||||||
cursorColor = Color.DarkGray,
|
|
||||||
),
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoginListSortMenu(
|
|
||||||
showMenu: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
store: LoginsStore,
|
|
||||||
) {
|
|
||||||
val sortOrder by store.observeAsState(store.state.sortOrder) { store.state.sortOrder }
|
|
||||||
DropdownMenu(
|
|
||||||
menuItems = listOf(
|
|
||||||
MenuItem.CheckableItem(
|
|
||||||
text = mozilla.components.compose.base.text.Text.Resource(
|
|
||||||
R.string.saved_logins_sort_strategy_alphabetically,
|
|
||||||
),
|
|
||||||
onClick = { store.dispatch(LoginsListSortMenuAction.OrderByNameClicked) },
|
|
||||||
isChecked = sortOrder == LoginsSortOrder.Alphabetical,
|
|
||||||
),
|
|
||||||
MenuItem.CheckableItem(
|
|
||||||
text = mozilla.components.compose.base.text.Text.Resource(
|
|
||||||
R.string.saved_logins_sort_strategy_last_used,
|
|
||||||
),
|
|
||||||
onClick = { store.dispatch(LoginsListSortMenuAction.OrderByLastUsedClicked) },
|
|
||||||
isChecked = sortOrder == LoginsSortOrder.LastUsed,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
expanded = showMenu,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val LOGINS_LIST_SIZE = 15
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@FlexibleWindowLightDarkPreview
|
|
||||||
private fun LoginsListScreenPreview() {
|
|
||||||
val loginItems = List(LOGINS_LIST_SIZE) {
|
|
||||||
LoginItem(
|
|
||||||
guid = "$it",
|
|
||||||
url = "https://www.justanothersite$it.com",
|
|
||||||
username = "username $it",
|
|
||||||
password = "password $it",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val store = { _: NavHostController ->
|
|
||||||
LoginsStore(
|
|
||||||
initialState = LoginsState(
|
|
||||||
loginItems = loginItems,
|
|
||||||
searchText = "",
|
|
||||||
sortOrder = LoginsSortOrder.default,
|
|
||||||
biometricAuthenticationDialogState = null,
|
|
||||||
loginsListState = null,
|
|
||||||
loginsAddLoginState = null,
|
|
||||||
loginsEditLoginState = null,
|
|
||||||
loginsLoginDetailState = null,
|
|
||||||
loginsDeletionDialogState = null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FirefoxTheme {
|
|
||||||
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
|
|
||||||
SavedLoginsScreen(store)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@FlexibleWindowLightDarkPreview
|
|
||||||
private fun EmptyLoginsListScreenPreview() {
|
|
||||||
val store = { _: NavHostController ->
|
|
||||||
LoginsStore(
|
|
||||||
initialState = LoginsState(
|
|
||||||
loginItems = listOf(),
|
|
||||||
searchText = "",
|
|
||||||
sortOrder = LoginsSortOrder.default,
|
|
||||||
biometricAuthenticationDialogState = null,
|
|
||||||
loginsListState = null,
|
|
||||||
loginsAddLoginState = null,
|
|
||||||
loginsEditLoginState = null,
|
|
||||||
loginsLoginDetailState = null,
|
|
||||||
loginsDeletionDialogState = null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FirefoxTheme {
|
|
||||||
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
|
|
||||||
SavedLoginsScreen(store)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2356,11 +2356,6 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
|||||||
default = false,
|
default = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
var loginsListSortOrder by stringPreference(
|
|
||||||
key = appContext.getPreferenceKey(R.string.pref_key_logins_list_sort_order),
|
|
||||||
default = "",
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether or not to show the entry point for the DNS over HTTPS settings
|
* Indicates whether or not to show the entry point for the DNS over HTTPS settings
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -407,6 +407,7 @@
|
|||||||
<string name="pref_key_enable_compose_top_sites" translatable="false">pref_key_enable_compose_top_sites</string>
|
<string name="pref_key_enable_compose_top_sites" translatable="false">pref_key_enable_compose_top_sites</string>
|
||||||
<string name="pref_key_enable_compose_homepage" translatable="false">pref_key_enable_compose_homepage</string>
|
<string name="pref_key_enable_compose_homepage" translatable="false">pref_key_enable_compose_homepage</string>
|
||||||
<string name="pref_key_enable_homepage_searchbar" translatable="false">pref_key_enable_homepage_searchbar</string>
|
<string name="pref_key_enable_homepage_searchbar" translatable="false">pref_key_enable_homepage_searchbar</string>
|
||||||
|
<string name="pref_key_enable_compose_logins" translatable="false">pref_key_enable_compose_logins</string>
|
||||||
<string name="pref_key_enable_menu_redesign" translatable="false">pref_key_enable_menu_redesign</string>
|
<string name="pref_key_enable_menu_redesign" translatable="false">pref_key_enable_menu_redesign</string>
|
||||||
<string name="pref_key_enable_homepage_as_new_tab" translatable="false">pref_key_enable_homepage_as_new_tab</string>
|
<string name="pref_key_enable_homepage_as_new_tab" translatable="false">pref_key_enable_homepage_as_new_tab</string>
|
||||||
<string name="pref_key_enable_unified_trust_panel" translatable="false">pref_key_enable_unified_trust_panel</string>
|
<string name="pref_key_enable_unified_trust_panel" translatable="false">pref_key_enable_unified_trust_panel</string>
|
||||||
@@ -415,10 +416,6 @@
|
|||||||
<string name="pref_key_enable_shortcuts_suggestions" translatable="false">pref_key_enable_shortcuts_suggestions</string>
|
<string name="pref_key_enable_shortcuts_suggestions" translatable="false">pref_key_enable_shortcuts_suggestions</string>
|
||||||
<string name="pref_key_enable_composable_toolbar" translatable="false">pref_key_enable_composable_toolbar</string>"
|
<string name="pref_key_enable_composable_toolbar" translatable="false">pref_key_enable_composable_toolbar</string>"
|
||||||
|
|
||||||
<!-- Logins -->
|
|
||||||
<string name="pref_key_enable_compose_logins" translatable="false">pref_key_enable_compose_logins</string>
|
|
||||||
<string name="pref_key_logins_list_sort_order" translatable="false">pref_key_logins_list_sort_order</string>
|
|
||||||
|
|
||||||
<!-- Growth Data -->
|
<!-- Growth Data -->
|
||||||
<string name="pref_key_growth_set_as_default" translatable="false">pref_key_growth_set_as_default</string>
|
<string name="pref_key_growth_set_as_default" translatable="false">pref_key_growth_set_as_default</string>
|
||||||
<string name="pref_key_growth_first_week_series_sent" translatable="false">pref_key_growth_first_week_series_sent</string>
|
<string name="pref_key_growth_first_week_series_sent" translatable="false">pref_key_growth_first_week_series_sent</string>
|
||||||
|
|||||||
@@ -2299,8 +2299,6 @@
|
|||||||
<string name="preferences_passwords_exceptions_remove_all">Delete all exceptions</string>
|
<string name="preferences_passwords_exceptions_remove_all">Delete all exceptions</string>
|
||||||
<!-- Hint for search box in passwords list -->
|
<!-- Hint for search box in passwords list -->
|
||||||
<string name="preferences_passwords_saved_logins_search_2">Search passwords</string>
|
<string name="preferences_passwords_saved_logins_search_2">Search passwords</string>
|
||||||
<!-- Content description for the logins navigation bar back button -->
|
|
||||||
<string name="logins_navigate_back_button_content_description">Back</string>
|
|
||||||
<!-- The header for the site that a login is for -->
|
<!-- The header for the site that a login is for -->
|
||||||
<string name="preferences_passwords_saved_logins_site">Site</string>
|
<string name="preferences_passwords_saved_logins_site">Site</string>
|
||||||
<!-- The header for the username for a login -->
|
<!-- The header for the username for a login -->
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import mozilla.components.concept.storage.Login
|
|
||||||
import mozilla.components.concept.storage.LoginsStorage
|
|
||||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
|
||||||
import mozilla.components.support.test.mock
|
|
||||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
|
||||||
import mozilla.components.support.test.rule.runTestOnMain
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mockito.Mockito.verify
|
|
||||||
import org.mockito.Mockito.`when`
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class LoginsMiddlewareTest {
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val coroutineRule = MainCoroutineRule()
|
|
||||||
|
|
||||||
private lateinit var loginsStorage: LoginsStorage
|
|
||||||
private lateinit var clipboardManager: ClipboardManager
|
|
||||||
private lateinit var navController: NavController
|
|
||||||
private lateinit var exitLogins: () -> Unit
|
|
||||||
private lateinit var openTab: (String, Boolean) -> Unit
|
|
||||||
private lateinit var persistLoginsSortOrder: suspend (LoginsSortOrder) -> Unit
|
|
||||||
|
|
||||||
private val loginList = List(5) {
|
|
||||||
Login(guid = "guid$it", origin = "origin$it", username = "username$it", password = "password$it")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
loginsStorage = mock()
|
|
||||||
clipboardManager = mock()
|
|
||||||
navController = mock()
|
|
||||||
exitLogins = { }
|
|
||||||
openTab = { _, _ -> }
|
|
||||||
persistLoginsSortOrder = { }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN no logins in storage WHEN store is initialized THEN list of logins will be empty`() = runTestOnMain {
|
|
||||||
`when`(loginsStorage.list()).thenReturn(listOf())
|
|
||||||
val middleware = buildMiddleware()
|
|
||||||
val store = middleware.makeStore()
|
|
||||||
store.waitUntilIdle()
|
|
||||||
|
|
||||||
assertEquals(0, store.state.loginItems.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN current screen is list logins WHEN add password is clicked THEN navigate to add login screen`() {
|
|
||||||
val middleware = buildMiddleware()
|
|
||||||
val store = middleware.makeStore()
|
|
||||||
store.dispatch(InitAdd)
|
|
||||||
store.waitUntilIdle()
|
|
||||||
verify(navController).navigate(LoginsDestinations.ADD_LOGIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN current screen is list logins WHEN a login is clicked THEN navigate to edit login screen`() {
|
|
||||||
val middleware = buildMiddleware()
|
|
||||||
val store = middleware.makeStore()
|
|
||||||
|
|
||||||
store.dispatch(
|
|
||||||
DetailLoginMenuAction.EditLoginMenuItemClicked(
|
|
||||||
LoginItem(
|
|
||||||
guid = "guid1",
|
|
||||||
url = "url1",
|
|
||||||
username = "u1",
|
|
||||||
password = "p1",
|
|
||||||
timeLastUsed = 0L,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
store.waitUntilIdle()
|
|
||||||
|
|
||||||
verify(navController).navigate(LoginsDestinations.EDIT_LOGIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN current screen is list and the top-level is loaded WHEN back is clicked THEN exit logins`() = runTestOnMain {
|
|
||||||
`when`(loginsStorage.list()).thenReturn(loginList)
|
|
||||||
var exited = false
|
|
||||||
exitLogins = { exited = true }
|
|
||||||
val middleware = buildMiddleware()
|
|
||||||
val store = middleware.makeStore()
|
|
||||||
|
|
||||||
store.dispatch(LoginsListBackClicked)
|
|
||||||
store.waitUntilIdle()
|
|
||||||
|
|
||||||
assertTrue(exited)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildMiddleware() = LoginsMiddleware(
|
|
||||||
loginsStorage = loginsStorage,
|
|
||||||
getNavController = { navController },
|
|
||||||
exitLogins = exitLogins,
|
|
||||||
openTab = openTab,
|
|
||||||
ioDispatcher = coroutineRule.testDispatcher,
|
|
||||||
persistLoginsSortOrder = persistLoginsSortOrder,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun LoginsMiddleware.makeStore(
|
|
||||||
initialState: LoginsState = LoginsState(),
|
|
||||||
) = LoginsStore(
|
|
||||||
initialState = initialState,
|
|
||||||
middleware = listOf(this),
|
|
||||||
).also {
|
|
||||||
it.waitUntilIdle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/* 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 org.mozilla.fenix.settings.logins.ui
|
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class LoginsReducerTest {
|
|
||||||
@Test
|
|
||||||
fun `WHEN store initializes THEN no changes to state`() {
|
|
||||||
val state = LoginsState()
|
|
||||||
|
|
||||||
assertEquals(state, loginsReducer(state, Init))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN logins are loaded THEN they are added to state`() {
|
|
||||||
val state = LoginsState()
|
|
||||||
val items = List(5) {
|
|
||||||
LoginItem(
|
|
||||||
guid = "$it",
|
|
||||||
url = "url",
|
|
||||||
username = "user$it",
|
|
||||||
password = "pass$it",
|
|
||||||
timeLastUsed = System.currentTimeMillis(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = loginsReducer(
|
|
||||||
state,
|
|
||||||
LoginsLoaded(
|
|
||||||
loginItems = items,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val expected = state.copy(
|
|
||||||
loginItems = items,
|
|
||||||
)
|
|
||||||
assertEquals(expected, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN we are on the list logins screen WHEN add login is clicked THEN initialize the add login state`() {
|
|
||||||
val state = LoginsState().copy(loginsAddLoginState = LoginsAddLoginState.None)
|
|
||||||
|
|
||||||
val result = loginsReducer(state, InitAdd)
|
|
||||||
|
|
||||||
assertEquals(LoginsAddLoginState.None, result.loginsAddLoginState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN there is no substate screen present WHEN back is clicked THEN state is unchanged`() {
|
|
||||||
val state = LoginsState()
|
|
||||||
|
|
||||||
val result = loginsReducer(state, LoginsListBackClicked)
|
|
||||||
|
|
||||||
assertEquals(LoginsState(), result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a logins list WHEN the alphabetical sort menu item is clicked THEN sort the logins list`() {
|
|
||||||
val items = List(3) {
|
|
||||||
LoginItem(
|
|
||||||
guid = "$it",
|
|
||||||
url = "$it url",
|
|
||||||
username = "user$it",
|
|
||||||
password = "pass$it",
|
|
||||||
timeLastUsed = 0L + it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = LoginsState().copy(loginItems = items)
|
|
||||||
|
|
||||||
val alphabetical = loginsReducer(state, LoginsListSortMenuAction.OrderByNameClicked)
|
|
||||||
assertEquals(listOf(items[0], items[1], items[2]), alphabetical.loginItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a logins list WHEN the last used sort menu item is clicked THEN sort the logins list`() {
|
|
||||||
val items = List(3) {
|
|
||||||
LoginItem(
|
|
||||||
guid = "$it",
|
|
||||||
url = "$it url",
|
|
||||||
username = "user$it",
|
|
||||||
password = "pass$it",
|
|
||||||
timeLastUsed = 0L + it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = LoginsState().copy(loginItems = items)
|
|
||||||
|
|
||||||
val newest = loginsReducer(state, LoginsListSortMenuAction.OrderByLastUsedClicked)
|
|
||||||
assertEquals(listOf(items[2], items[1], items[0]), newest.loginItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a logins list WHEN the search is used THEN filter the logins list`() {
|
|
||||||
val items = List(7) {
|
|
||||||
LoginItem(
|
|
||||||
guid = "$it",
|
|
||||||
url = if (it % 2 == 0) "$it url" else "$it uri",
|
|
||||||
username = "user$it",
|
|
||||||
password = "pass$it",
|
|
||||||
timeLastUsed = System.currentTimeMillis(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = LoginsState().copy(loginItems = items)
|
|
||||||
|
|
||||||
val filterUrl = loginsReducer(state, SearchLogins("url", items))
|
|
||||||
assertEquals("url", filterUrl.searchText)
|
|
||||||
assertEquals(4, filterUrl.loginItems.size)
|
|
||||||
assertEquals(listOf(items[0], items[2], items[4], items[6]), filterUrl.loginItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user