diff --git a/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/button/IconButton.kt b/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/button/IconButton.kt index 75b666350aeb..6c8b75288fe8 100644 --- a/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/button/IconButton.kt +++ b/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/button/IconButton.kt @@ -58,7 +58,7 @@ private val RippleRadius = 24.dp @Composable fun IconButton( onClick: () -> Unit, - contentDescription: String?, + contentDescription: String, modifier: Modifier = Modifier, onClickLabel: String? = null, enabled: Boolean = true, @@ -68,11 +68,7 @@ fun IconButton( val view = LocalView.current Box( modifier = modifier - .semantics { - if (contentDescription != null) { - this.contentDescription = contentDescription - } - } + .semantics { this.contentDescription = contentDescription } .minimumInteractiveComponentSize() .clickable( interactionSource = interactionSource, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/lifecycle/LifecycleHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/lifecycle/LifecycleHolder.kt deleted file mode 100644 index dffcfb1c0bd4..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/lifecycle/LifecycleHolder.kt +++ /dev/null @@ -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, -) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt index 7f23eb7abafb..3c81d9f1afa5 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt @@ -19,8 +19,6 @@ import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.os.bundleOf import androidx.core.view.MenuProvider @@ -29,9 +27,7 @@ import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavHostController import androidx.navigation.fragment.findNavController -import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.menu.MenuController import mozilla.components.concept.menu.Orientation 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.databinding.FragmentSavedLoginsBinding import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.registerForActivityResult import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar -import org.mozilla.fenix.lifecycle.LifecycleHolder import org.mozilla.fenix.settings.biometric.DefaultBiometricUtils import org.mozilla.fenix.settings.logins.LoginsAction 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.createInitialLoginsListState 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.theme.FirefoxTheme @SuppressWarnings("TooManyFunctions") class SavedLoginsFragment : SecureFragment(), MenuProvider { @@ -91,9 +78,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - if (requireContext().settings().enableComposeLogins) { return } - startForResult = registerForActivityResult { BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt = false @@ -105,12 +89,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider { override fun onResume() { super.onResume() - - if (requireContext().settings().enableComposeLogins) { - hideToolbar() - return - } - if (BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt) { BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt = false @@ -141,72 +119,11 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider { initToolbar() } - @Suppress("LongMethod") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): 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) _binding = FragmentSavedLoginsBinding.bind(view) @@ -252,10 +169,6 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (requireContext().settings().enableComposeLogins) { - return - } - setFragmentResultListener(LoginDetailFragment.LOGIN_REQUEST_KEY) { _, bundle -> removedLoginGuid = bundle.getString(LoginDetailFragment.LOGIN_BUNDLE_ARGS) 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 */ override fun onPause() { - if (requireContext().settings().enableComposeLogins) { - super.onPause() - return - } - toolbarChildContainer.removeAllViews() toolbarChildContainer.visibility = View.GONE (activity as HomeActivity).getSupportActionBarAndInflateIfNecessary() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/DefaultSavedLoginsStorage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/DefaultSavedLoginsStorage.kt deleted file mode 100644 index f7a96fc06bd6..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/DefaultSavedLoginsStorage.kt +++ /dev/null @@ -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 - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginItem.kt deleted file mode 100644 index a6d76e6e0482..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginItem.kt +++ /dev/null @@ -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, -) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsAction.kt deleted file mode 100644 index f0884cfdc82d..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsAction.kt +++ /dev/null @@ -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, -) : 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) : - 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() -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsMiddleware.kt deleted file mode 100644 index 6b9980d140c6..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsMiddleware.kt +++ /dev/null @@ -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 { - - private val scope = CoroutineScope(ioDispatcher) - - @Suppress("LongMethod", "ComplexMethod") - override fun invoke( - context: MiddlewareContext, - 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.loadLoginsList() = scope.launch { - val loginItems = arrayListOf() - 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)) - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsReducer.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsReducer.kt deleted file mode 100644 index 0fd45f22e283..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsReducer.kt +++ /dev/null @@ -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 - } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsState.kt deleted file mode 100644 index 6ddb3012e569..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsState.kt +++ /dev/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 = 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, -) - -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 - - /** - * Represents the ordering of the logins list when sorted alphabetically. - */ - data object Alphabetical : LoginsSortOrder() { - override val asString: String - get() = "alphabetical" - - override val comparator: Comparator - 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 - 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 - } - } - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsStore.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsStore.kt deleted file mode 100644 index 16e64c222350..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsStore.kt +++ /dev/null @@ -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 = ::loginsReducer, - middleware: List> = listOf(), - val lifecycleHolder: LifecycleHolder? = null, - loginToLoad: String? = null, -) : UiStore( - initialState = initialState, - reducer = reducer, - middleware = middleware, -) { - init { - val action = loginToLoad?.let { InitEdit(loginToLoad) } ?: Init - dispatch(action) - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt deleted file mode 100644 index 96278cf2323b..000000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt +++ /dev/null @@ -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) - } - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 09ac34db9892..c24eeb56c0c1 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -2356,11 +2356,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { 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 */ diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml index ee13e450db44..97c268676ff6 100644 --- a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml +++ b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml @@ -407,6 +407,7 @@ pref_key_enable_compose_top_sites pref_key_enable_compose_homepage pref_key_enable_homepage_searchbar + pref_key_enable_compose_logins pref_key_enable_menu_redesign pref_key_enable_homepage_as_new_tab pref_key_enable_unified_trust_panel @@ -415,10 +416,6 @@ pref_key_enable_shortcuts_suggestions pref_key_enable_composable_toolbar" - - pref_key_enable_compose_logins - pref_key_logins_list_sort_order - pref_key_growth_set_as_default pref_key_growth_first_week_series_sent diff --git a/mobile/android/fenix/app/src/main/res/values/strings.xml b/mobile/android/fenix/app/src/main/res/values/strings.xml index f9745758d6ad..6e87ab428a3d 100644 --- a/mobile/android/fenix/app/src/main/res/values/strings.xml +++ b/mobile/android/fenix/app/src/main/res/values/strings.xml @@ -2299,8 +2299,6 @@ Delete all exceptions Search passwords - - Back Site diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/logins/ui/LoginsMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/logins/ui/LoginsMiddlewareTest.kt deleted file mode 100644 index 03ee3f9a3090..000000000000 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/logins/ui/LoginsMiddlewareTest.kt +++ /dev/null @@ -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() - } -} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/logins/ui/LoginsReducerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/logins/ui/LoginsReducerTest.kt deleted file mode 100644 index c6ef5f7563bb..000000000000 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/logins/ui/LoginsReducerTest.kt +++ /dev/null @@ -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) - } -}