Skip to content

Commit afcda48

Browse files
feat: Login through username in the new app (#181)
- Add username support for the authentication fixes: LEARNER-9782
1 parent 4d97d4f commit afcda48

File tree

7 files changed

+72
-30
lines changed

7 files changed

+72
-30
lines changed

auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,34 @@ import android.view.LayoutInflater
66
import android.view.ViewGroup
77
import androidx.compose.foundation.Image
88
import androidx.compose.foundation.background
9-
import androidx.compose.foundation.layout.*
9+
import androidx.compose.foundation.layout.Arrangement
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxHeight
14+
import androidx.compose.foundation.layout.fillMaxSize
15+
import androidx.compose.foundation.layout.fillMaxWidth
16+
import androidx.compose.foundation.layout.height
17+
import androidx.compose.foundation.layout.navigationBarsPadding
18+
import androidx.compose.foundation.layout.padding
19+
import androidx.compose.foundation.layout.size
20+
import androidx.compose.foundation.layout.widthIn
1021
import androidx.compose.foundation.rememberScrollState
1122
import androidx.compose.foundation.verticalScroll
12-
import androidx.compose.material.*
13-
import androidx.compose.runtime.*
23+
import androidx.compose.material.CircularProgressIndicator
24+
import androidx.compose.material.Icon
25+
import androidx.compose.material.MaterialTheme
26+
import androidx.compose.material.Scaffold
27+
import androidx.compose.material.Surface
28+
import androidx.compose.material.Text
29+
import androidx.compose.material.rememberScaffoldState
30+
import androidx.compose.runtime.Composable
31+
import androidx.compose.runtime.getValue
1432
import androidx.compose.runtime.livedata.observeAsState
33+
import androidx.compose.runtime.mutableStateOf
34+
import androidx.compose.runtime.remember
1535
import androidx.compose.runtime.saveable.rememberSaveable
36+
import androidx.compose.runtime.setValue
1637
import androidx.compose.ui.Alignment
1738
import androidx.compose.ui.Modifier
1839
import androidx.compose.ui.graphics.Color
@@ -28,18 +49,26 @@ import androidx.compose.ui.tooling.preview.Preview
2849
import androidx.compose.ui.unit.Dp
2950
import androidx.compose.ui.unit.dp
3051
import androidx.fragment.app.Fragment
52+
import org.koin.androidx.viewmodel.ext.android.viewModel
3153
import org.openedx.auth.presentation.ui.LoginTextField
54+
import org.openedx.core.AppUpdateState
55+
import org.openedx.core.R
3256
import org.openedx.core.UIMessage
33-
import org.openedx.core.ui.*
57+
import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen
58+
import org.openedx.core.ui.BackBtn
59+
import org.openedx.core.ui.HandleUIMessage
60+
import org.openedx.core.ui.OpenEdXButton
61+
import org.openedx.core.ui.WindowSize
62+
import org.openedx.core.ui.WindowType
63+
import org.openedx.core.ui.displayCutoutForLandscape
64+
import org.openedx.core.ui.rememberWindowSize
65+
import org.openedx.core.ui.statusBarsInset
3466
import org.openedx.core.ui.theme.OpenEdXTheme
3567
import org.openedx.core.ui.theme.appColors
3668
import org.openedx.core.ui.theme.appShapes
3769
import org.openedx.core.ui.theme.appTypography
38-
import org.koin.androidx.viewmodel.ext.android.viewModel
39-
import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen
40-
import org.openedx.core.AppUpdateState
70+
import org.openedx.core.ui.windowSizeValue
4171
import org.openedx.auth.R as authR
42-
import org.openedx.core.R
4372

4473
class RestorePasswordFragment : Fragment() {
4574

@@ -226,6 +255,8 @@ private fun RestorePasswordScreen(
226255
Spacer(modifier = Modifier.height(32.dp))
227256
LoginTextField(
228257
modifier = Modifier.fillMaxWidth(),
258+
title = stringResource(id = authR.string.auth_email),
259+
description = stringResource(id = authR.string.auth_example_email),
229260
onValueChanged = {
230261
email = it
231262
},

auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ class SignInViewModel(
7272
}
7373

7474
fun login(username: String, password: String) {
75-
if (!validator.isEmailValid(username)) {
75+
if (!validator.isEmailOrUserNameValid(username)) {
7676
_uiMessage.value =
77-
UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_email))
77+
UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_email_username))
7878
return
7979
}
8080
if (!validator.isPasswordValid(password)) {

auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ private fun AuthForm(
209209
LoginTextField(
210210
modifier = Modifier
211211
.fillMaxWidth(),
212+
title = stringResource(id = R.string.auth_email_username),
213+
description = stringResource(id = R.string.auth_enter_email_username),
212214
onValueChanged = {
213215
login = it
214216
})

auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ fun OptionalFields(
214214
@Composable
215215
fun LoginTextField(
216216
modifier: Modifier = Modifier,
217+
title: String,
218+
description: String,
217219
onValueChanged: (String) -> Unit,
218220
imeAction: ImeAction = ImeAction.Next,
219221
keyboardActions: (FocusManager) -> Unit = { it.moveFocus(FocusDirection.Down) }
@@ -226,7 +228,7 @@ fun LoginTextField(
226228
val focusManager = LocalFocusManager.current
227229
Text(
228230
modifier = Modifier.fillMaxWidth(),
229-
text = stringResource(id = R.string.auth_email),
231+
text = title,
230232
color = MaterialTheme.appColors.textPrimary,
231233
style = MaterialTheme.appTypography.labelLarge
232234
)
@@ -244,7 +246,7 @@ fun LoginTextField(
244246
shape = MaterialTheme.appShapes.textFieldShape,
245247
placeholder = {
246248
Text(
247-
text = stringResource(id = R.string.auth_example_email),
249+
text = description,
248250
color = MaterialTheme.appColors.textFieldHint,
249251
style = MaterialTheme.appTypography.bodyMedium
250252
)

auth/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
<string name="auth_forgot_password">Forgot password?</string>
99
<string name="auth_email">Email</string>
1010
<string name="auth_invalid_email">Invalid email</string>
11+
<string name="auth_email_username" tools:ignore="MissingTranslation">Email or Username</string>
12+
<string name="auth_invalid_email_username" tools:ignore="MissingTranslation">Invalid email or username</string>
1113
<string name="auth_invalid_password">Password is too short</string>
1214
<string name="auth_welcome_back">Welcome back! Please authorize to continue.</string>
1315
<string name="auth_show_optional_fields">Show optional fields</string>
@@ -19,6 +21,7 @@
1921
<string name="auth_check_your_email">Check your email</string>
2022
<string name="auth_restore_password_success">We have sent a password recover instructions to your email %s</string>
2123
<string name="auth_example_email" translatable="false">[email protected]</string>
24+
<string name="auth_enter_email_username" tools:ignore="MissingTranslation">Enter email or username</string>
2225
<string name="auth_enter_password">Enter password</string>
2326
<string name="auth_create_new_account">Create new account.</string>
2427
<string name="auth_google" tools:ignore="ExtraTranslation">Sign in with Google</string>

auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class SignInViewModelTest {
6363
private val invalidCredential = "Invalid credentials"
6464
private val noInternet = "Slow or no internet connection"
6565
private val somethingWrong = "Something went wrong"
66-
private val invalidEmail = "Invalid email"
66+
private val invalidEmailOrUsername = "Invalid email or username"
6767
private val invalidPassword = "Password too short"
6868

6969
private val user = User(0, "", "", "")
@@ -74,7 +74,7 @@ class SignInViewModelTest {
7474
every { resourceManager.getString(CoreRes.string.core_error_invalid_grant) } returns invalidCredential
7575
every { resourceManager.getString(CoreRes.string.core_error_no_connection) } returns noInternet
7676
every { resourceManager.getString(CoreRes.string.core_error_unknown_error) } returns somethingWrong
77-
every { resourceManager.getString(R.string.auth_invalid_email) } returns invalidEmail
77+
every { resourceManager.getString(R.string.auth_invalid_email_username) } returns invalidEmailOrUsername
7878
every { resourceManager.getString(R.string.auth_invalid_password) } returns invalidPassword
7979
every { appUpgradeNotifier.notifier } returns emptyFlow()
8080
every { config.isPreLoginExperienceEnabled() } returns false
@@ -91,7 +91,7 @@ class SignInViewModelTest {
9191

9292
@Test
9393
fun `login empty credentials validation error`() = runTest {
94-
every { validator.isEmailValid(any()) } returns false
94+
every { validator.isEmailOrUserNameValid(any()) } returns false
9595
every { preferencesManager.user } returns user
9696
every { analytics.setUserIdForSession(any()) } returns Unit
9797
val viewModel = SignInViewModel(
@@ -113,14 +113,14 @@ class SignInViewModelTest {
113113

114114
val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage
115115
val uiState = viewModel.uiState.value
116-
assertEquals(invalidEmail, message.message)
116+
assertEquals(invalidEmailOrUsername, message.message)
117117
assertFalse(uiState.showProgress)
118118
assertFalse(uiState.loginSuccess)
119119
}
120120

121121
@Test
122122
fun `login invalid email validation error`() = runTest {
123-
every { validator.isEmailValid(any()) } returns false
123+
every { validator.isEmailOrUserNameValid(any()) } returns false
124124
every { preferencesManager.user } returns user
125125
every { analytics.setUserIdForSession(any()) } returns Unit
126126
val viewModel = SignInViewModel(
@@ -142,14 +142,14 @@ class SignInViewModelTest {
142142

143143
val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage
144144
val uiState = viewModel.uiState.value
145-
assertEquals(invalidEmail, message.message)
145+
assertEquals(invalidEmailOrUsername, message.message)
146146
assertFalse(uiState.showProgress)
147147
assertFalse(uiState.loginSuccess)
148148
}
149149

150150
@Test
151151
fun `login empty password validation error`() = runTest {
152-
every { validator.isEmailValid(any()) } returns true
152+
every { validator.isEmailOrUserNameValid(any()) } returns true
153153
every { validator.isPasswordValid(any()) } returns false
154154
every { preferencesManager.user } returns user
155155
every { analytics.setUserIdForSession(any()) } returns Unit
@@ -180,7 +180,7 @@ class SignInViewModelTest {
180180

181181
@Test
182182
fun `login invalid password validation error`() = runTest {
183-
every { validator.isEmailValid(any()) } returns true
183+
every { validator.isEmailOrUserNameValid(any()) } returns true
184184
every { validator.isPasswordValid(any()) } returns false
185185
every { preferencesManager.user } returns user
186186
every { analytics.setUserIdForSession(any()) } returns Unit
@@ -211,7 +211,7 @@ class SignInViewModelTest {
211211

212212
@Test
213213
fun `login success`() = runTest {
214-
every { validator.isEmailValid(any()) } returns true
214+
every { validator.isEmailOrUserNameValid(any()) } returns true
215215
every { validator.isPasswordValid(any()) } returns true
216216
every { analytics.userLoginEvent(any()) } returns Unit
217217
every { preferencesManager.user } returns user
@@ -245,7 +245,7 @@ class SignInViewModelTest {
245245

246246
@Test
247247
fun `login network error`() = runTest {
248-
every { validator.isEmailValid(any()) } returns true
248+
every { validator.isEmailOrUserNameValid(any()) } returns true
249249
every { validator.isPasswordValid(any()) } returns true
250250
every { preferencesManager.user } returns user
251251
every { analytics.setUserIdForSession(any()) } returns Unit
@@ -279,7 +279,7 @@ class SignInViewModelTest {
279279

280280
@Test
281281
fun `login invalid grant error`() = runTest {
282-
every { validator.isEmailValid(any()) } returns true
282+
every { validator.isEmailOrUserNameValid(any()) } returns true
283283
every { validator.isPasswordValid(any()) } returns true
284284
every { preferencesManager.user } returns user
285285
every { analytics.setUserIdForSession(any()) } returns Unit
@@ -313,7 +313,7 @@ class SignInViewModelTest {
313313

314314
@Test
315315
fun `login unknown exception`() = runTest {
316-
every { validator.isEmailValid(any()) } returns true
316+
every { validator.isEmailOrUserNameValid(any()) } returns true
317317
every { validator.isPasswordValid(any()) } returns true
318318
every { preferencesManager.user } returns user
319319
every { analytics.setUserIdForSession(any()) } returns Unit

core/src/main/java/org/openedx/core/Validator.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import java.util.regex.Pattern
44

55
class Validator {
66

7-
fun isEmailValid(email: String): Boolean {
8-
val validEmailAddressRegex =
9-
Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE)
10-
val matcher = validEmailAddressRegex.matcher(email)
11-
return matcher.find()
7+
fun isEmailOrUserNameValid(input: String): Boolean {
8+
return if (input.contains("@")) {
9+
val validEmailAddressRegex = Pattern.compile(
10+
"^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE
11+
)
12+
validEmailAddressRegex.matcher(input).find()
13+
} else {
14+
input.isNotBlank() && input.contains(" ").not()
15+
}
1216
}
1317

1418
fun isPasswordValid(password: String): Boolean {
1519
return password.length >= 2
1620
}
1721

18-
}
22+
}

0 commit comments

Comments
 (0)