From afcda480d035f8c1480079b8ba36695b46c87473 Mon Sep 17 00:00:00 2001 From: Farhan Arshad <43750646+farhan-arshad-dev@users.noreply.github.com> Date: Fri, 19 Jan 2024 19:36:18 +0500 Subject: [PATCH] feat: Login through username in the new app (#181) - Add username support for the authentication fixes: LEARNER-9782 --- .../restore/RestorePasswordFragment.kt | 47 +++++++++++++++---- .../presentation/signin/SignInViewModel.kt | 4 +- .../presentation/signin/compose/SignInView.kt | 2 + .../openedx/auth/presentation/ui/AuthUI.kt | 6 ++- auth/src/main/res/values/strings.xml | 3 ++ .../signin/SignInViewModelTest.kt | 24 +++++----- .../main/java/org/openedx/core/Validator.kt | 16 ++++--- 7 files changed, 72 insertions(+), 30 deletions(-) diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt index e644212b7..bd10b1092 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt @@ -6,13 +6,34 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -28,18 +49,26 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.auth.presentation.ui.LoginTextField +import org.openedx.core.AppUpdateState +import org.openedx.core.R import org.openedx.core.UIMessage -import org.openedx.core.ui.* +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen +import org.openedx.core.ui.BackBtn +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen -import org.openedx.core.AppUpdateState +import org.openedx.core.ui.windowSizeValue import org.openedx.auth.R as authR -import org.openedx.core.R class RestorePasswordFragment : Fragment() { @@ -226,6 +255,8 @@ private fun RestorePasswordScreen( Spacer(modifier = Modifier.height(32.dp)) LoginTextField( modifier = Modifier.fillMaxWidth(), + title = stringResource(id = authR.string.auth_email), + description = stringResource(id = authR.string.auth_example_email), onValueChanged = { email = it }, diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index 4ab688e21..172e1fc41 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -72,9 +72,9 @@ class SignInViewModel( } fun login(username: String, password: String) { - if (!validator.isEmailValid(username)) { + if (!validator.isEmailOrUserNameValid(username)) { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_email)) + UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_email_username)) return } if (!validator.isPasswordValid(password)) { diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt index 2aed9869c..efe82ca90 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt @@ -209,6 +209,8 @@ private fun AuthForm( LoginTextField( modifier = Modifier .fillMaxWidth(), + title = stringResource(id = R.string.auth_email_username), + description = stringResource(id = R.string.auth_enter_email_username), onValueChanged = { login = it }) diff --git a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt index e00b56e6c..d01519f8d 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt @@ -214,6 +214,8 @@ fun OptionalFields( @Composable fun LoginTextField( modifier: Modifier = Modifier, + title: String, + description: String, onValueChanged: (String) -> Unit, imeAction: ImeAction = ImeAction.Next, keyboardActions: (FocusManager) -> Unit = { it.moveFocus(FocusDirection.Down) } @@ -226,7 +228,7 @@ fun LoginTextField( val focusManager = LocalFocusManager.current Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.auth_email), + text = title, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.labelLarge ) @@ -244,7 +246,7 @@ fun LoginTextField( shape = MaterialTheme.appShapes.textFieldShape, placeholder = { Text( - text = stringResource(id = R.string.auth_example_email), + text = description, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.bodyMedium ) diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml index 9c77ef877..cc960f498 100644 --- a/auth/src/main/res/values/strings.xml +++ b/auth/src/main/res/values/strings.xml @@ -8,6 +8,8 @@ Forgot password? Email Invalid email + Email or Username + Invalid email or username Password is too short Welcome back! Please authorize to continue. Show optional fields @@ -19,6 +21,7 @@ Check your email We have sent a password recover instructions to your email %s username@domain.com + Enter email or username Enter password Create new account. Sign in with Google diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index 9e5e13be0..3da90a8fb 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -63,7 +63,7 @@ class SignInViewModelTest { private val invalidCredential = "Invalid credentials" private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" - private val invalidEmail = "Invalid email" + private val invalidEmailOrUsername = "Invalid email or username" private val invalidPassword = "Password too short" private val user = User(0, "", "", "") @@ -74,7 +74,7 @@ class SignInViewModelTest { every { resourceManager.getString(CoreRes.string.core_error_invalid_grant) } returns invalidCredential every { resourceManager.getString(CoreRes.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(CoreRes.string.core_error_unknown_error) } returns somethingWrong - every { resourceManager.getString(R.string.auth_invalid_email) } returns invalidEmail + every { resourceManager.getString(R.string.auth_invalid_email_username) } returns invalidEmailOrUsername every { resourceManager.getString(R.string.auth_invalid_password) } returns invalidPassword every { appUpgradeNotifier.notifier } returns emptyFlow() every { config.isPreLoginExperienceEnabled() } returns false @@ -91,7 +91,7 @@ class SignInViewModelTest { @Test fun `login empty credentials validation error`() = runTest { - every { validator.isEmailValid(any()) } returns false + every { validator.isEmailOrUserNameValid(any()) } returns false every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = SignInViewModel( @@ -113,14 +113,14 @@ class SignInViewModelTest { val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage val uiState = viewModel.uiState.value - assertEquals(invalidEmail, message.message) + assertEquals(invalidEmailOrUsername, message.message) assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) } @Test fun `login invalid email validation error`() = runTest { - every { validator.isEmailValid(any()) } returns false + every { validator.isEmailOrUserNameValid(any()) } returns false every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = SignInViewModel( @@ -142,14 +142,14 @@ class SignInViewModelTest { val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage val uiState = viewModel.uiState.value - assertEquals(invalidEmail, message.message) + assertEquals(invalidEmailOrUsername, message.message) assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) } @Test fun `login empty password validation error`() = runTest { - every { validator.isEmailValid(any()) } returns true + every { validator.isEmailOrUserNameValid(any()) } returns true every { validator.isPasswordValid(any()) } returns false every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit @@ -180,7 +180,7 @@ class SignInViewModelTest { @Test fun `login invalid password validation error`() = runTest { - every { validator.isEmailValid(any()) } returns true + every { validator.isEmailOrUserNameValid(any()) } returns true every { validator.isPasswordValid(any()) } returns false every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit @@ -211,7 +211,7 @@ class SignInViewModelTest { @Test fun `login success`() = runTest { - every { validator.isEmailValid(any()) } returns true + every { validator.isEmailOrUserNameValid(any()) } returns true every { validator.isPasswordValid(any()) } returns true every { analytics.userLoginEvent(any()) } returns Unit every { preferencesManager.user } returns user @@ -245,7 +245,7 @@ class SignInViewModelTest { @Test fun `login network error`() = runTest { - every { validator.isEmailValid(any()) } returns true + every { validator.isEmailOrUserNameValid(any()) } returns true every { validator.isPasswordValid(any()) } returns true every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit @@ -279,7 +279,7 @@ class SignInViewModelTest { @Test fun `login invalid grant error`() = runTest { - every { validator.isEmailValid(any()) } returns true + every { validator.isEmailOrUserNameValid(any()) } returns true every { validator.isPasswordValid(any()) } returns true every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit @@ -313,7 +313,7 @@ class SignInViewModelTest { @Test fun `login unknown exception`() = runTest { - every { validator.isEmailValid(any()) } returns true + every { validator.isEmailOrUserNameValid(any()) } returns true every { validator.isPasswordValid(any()) } returns true every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit diff --git a/core/src/main/java/org/openedx/core/Validator.kt b/core/src/main/java/org/openedx/core/Validator.kt index ca758a071..cb3a66ae6 100644 --- a/core/src/main/java/org/openedx/core/Validator.kt +++ b/core/src/main/java/org/openedx/core/Validator.kt @@ -4,15 +4,19 @@ import java.util.regex.Pattern class Validator { - fun isEmailValid(email: String): Boolean { - val validEmailAddressRegex = - Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE) - val matcher = validEmailAddressRegex.matcher(email) - return matcher.find() + fun isEmailOrUserNameValid(input: String): Boolean { + return if (input.contains("@")) { + val validEmailAddressRegex = Pattern.compile( + "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE + ) + validEmailAddressRegex.matcher(input).find() + } else { + input.isNotBlank() && input.contains(" ").not() + } } fun isPasswordValid(password: String): Boolean { return password.length >= 2 } -} \ No newline at end of file +}