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
+}