Skip to content

Commit

Permalink
Updated login screen
Browse files Browse the repository at this point in the history
# *Fix/issue 38 password keyboard behaviour*

## ♻️ Current situation & Problem
#38 


## ⚙️ Release Notes 

- Added Password Toogle Icon to Login Screen
- Added Login Form Validator
- Added Login Error Cases 
- Switched to OutlinedTextFields


## ✅ Testing
- Added CredentialRegisterManagerAuthTest
- Added LoginViewModelTest
- Added LoginFormValidatorTest


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Signed-off-by: Basler182 <[email protected]>
  • Loading branch information
Basler182 authored Jun 22, 2024
1 parent 1f3a66f commit 9c33498
Show file tree
Hide file tree
Showing 20 changed files with 925 additions and 251 deletions.
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Spezi">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,17 @@ class MainActivity : ComponentActivity() {
Routes.InvitationCodeScreen
)

is OnboardingNavigationEvent.OnboardingScreen -> navHostController.navigate(Routes.OnboardingScreen)
is OnboardingNavigationEvent.OnboardingScreen -> navHostController.navigate(
Routes.OnboardingScreen
)

is OnboardingNavigationEvent.SequentialOnboardingScreen -> navHostController.navigate(
Routes.SequentialOnboardingScreen
)

is OnboardingNavigationEvent.ConsentScreen -> navHostController.navigate(Routes.ConsentScreen)
is OnboardingNavigationEvent.ConsentScreen -> navHostController.navigate(
Routes.ConsentScreen
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package edu.stanford.spezi.core.design.component.validated.outlinedtextfield

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import edu.stanford.spezi.core.design.theme.Colors
import edu.stanford.spezi.core.design.theme.TextStyles.labelSmall
import edu.stanford.spezi.core.utils.ComposableBlock

@Composable
fun ValidatedOutlinedTextField(
Expand All @@ -22,6 +23,12 @@ fun ValidatedOutlinedTextField(
onValueChange: (String) -> Unit,
labelText: String = "",
errorText: String? = null,
singleLine: Boolean = true,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
visualTransformation: VisualTransformation = VisualTransformation.None,
readOnly: Boolean = false,
trailingIcon: ComposableBlock? = null,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedTextField(
Expand All @@ -31,17 +38,17 @@ fun ValidatedOutlinedTextField(
onValueChange(it)
},
label = { Text(labelText) },
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
singleLine = singleLine,
keyboardOptions = keyboardOptions,
isError = errorText != null,
readOnly = readOnly,
trailingIcon = trailingIcon,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
supportingText = errorText?.let {
{ Text(text = it) }
},
)
if (errorText != null) {
Text(
text = errorText,
style = labelSmall,
color = Colors.error
)
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions core/design/src/main/res/drawable/ic_visibility.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/black"
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z" />
</vector>
10 changes: 10 additions & 0 deletions core/design/src/main/res/drawable/ic_visibility_off.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/black"
android:pathData="M644,532L586,474Q595,427 559,386Q523,345 466,354L408,296Q425,288 442.5,284Q460,280 480,280Q555,280 607.5,332.5Q660,385 660,460Q660,480 656,497.5Q652,515 644,532ZM772,658L714,602Q752,573 781.5,538.5Q811,504 832,460Q782,359 688.5,299.5Q595,240 480,240Q451,240 423,244Q395,248 368,256L306,194Q347,177 390,168.5Q433,160 480,160Q631,160 749,243.5Q867,327 920,460Q897,519 859.5,569.5Q822,620 772,658ZM792,904L624,738Q589,749 553.5,754.5Q518,760 480,760Q329,760 211,676.5Q93,593 40,460Q61,407 93,361.5Q125,316 166,280L56,168L112,112L848,848L792,904ZM222,336Q193,362 169,393Q145,424 128,460Q178,561 271.5,620.5Q365,680 480,680Q500,680 519,677.5Q538,675 558,672L522,634Q511,637 501,638.5Q491,640 480,640Q405,640 352.5,587.5Q300,535 300,460Q300,449 301.5,439Q303,429 306,418L222,336ZM541,429L541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429ZM390,504Q390,504 390,504Q390,504 390,504L390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Z" />
</vector>
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package edu.stanford.spezi.module.account.cred.manager

import android.content.Context
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
Expand Down Expand Up @@ -33,13 +32,10 @@ class CredentialLoginManagerAuth @Inject constructor(
suspend fun handlePasswordSignIn(
username: String,
password: String,
): Boolean {
val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
val createCredential = credentialManager.createCredential(context, createPasswordRequest)
if (createCredential.type == PasswordCredential.TYPE_PASSWORD_CREDENTIAL) {
return firebaseAuthManager.signInWithEmailAndPassword(username, password)
): Result<Unit> {
return runCatching {
firebaseAuthManager.signInWithEmailAndPassword(username, password)
}
return false
}

private suspend fun getCredential(filterByAuthorizedAccounts: Boolean): GoogleIdTokenCredential? {
Expand Down Expand Up @@ -67,7 +63,8 @@ class CredentialLoginManagerAuth @Inject constructor(
when (val credential = response.credential) {
is CustomCredential -> {
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
val googleIdTokenCredential =
GoogleIdTokenCredential.createFrom(credential.data)
return googleIdTokenCredential
}
if (credential.type == PasswordCredential.TYPE_PASSWORD_CREDENTIAL) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package edu.stanford.spezi.module.account.login

import edu.stanford.spezi.module.account.register.FormValidator
import javax.inject.Inject

internal class LoginFormValidator @Inject constructor() : FormValidator() {

fun isFormValid(uiState: UiState): Boolean {
return isValidEmail(uiState.email.value).isValid &&
isValidPassword(uiState.password.value).isValid
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,54 @@

package edu.stanford.spezi.module.account.login

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.hilt.navigation.compose.hiltViewModel
import edu.stanford.spezi.core.design.component.validated.outlinedtextfield.ValidatedOutlinedTextField
import edu.stanford.spezi.core.design.theme.Spacings
import edu.stanford.spezi.core.design.theme.SpeziTheme
import edu.stanford.spezi.core.design.theme.TextStyles.bodyLarge
import edu.stanford.spezi.core.design.theme.TextStyles.titleLarge
import edu.stanford.spezi.core.utils.extensions.testIdentifier
import edu.stanford.spezi.module.account.login.components.SignInWithGoogleButton
import edu.stanford.spezi.module.account.login.components.TextDivider
import edu.stanford.spezi.module.account.register.FieldState
import edu.stanford.spezi.module.account.register.IconLeadingContent
import edu.stanford.spezi.core.design.R as DesignR

@Composable
fun LoginScreen(
Expand All @@ -54,11 +69,23 @@ internal fun LoginScreen(
uiState: UiState,
onAction: (Action) -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current

Column(
modifier = Modifier
.testIdentifier(LoginScreenTestIdentifier.ROOT)
.fillMaxSize()
.padding(Spacings.medium),
.imePadding()
.verticalScroll(rememberScrollState())
.padding(Spacings.medium)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
println("Hide Keyboard")
keyboardController?.hide()
}
)
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Expand All @@ -74,33 +101,57 @@ You may login to your existing account or create a new one if you don't have one
style = bodyLarge,
)
Spacer(modifier = Modifier.height(Spacings.large))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = uiState.email,
onValueChange = { email ->
onAction(Action.TextFieldUpdate(email, TextFieldType.EMAIL))
},
label = { Text("E-Mail Address") },
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next)
)
IconLeadingContent(
icon = Icons.Outlined.Email,
content = {
ValidatedOutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = uiState.email.value,
errorText = uiState.email.error,
onValueChange = { email ->
onAction(Action.TextFieldUpdate(email, TextFieldType.EMAIL))
},
labelText = "E-Mail Address",
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next)
)
})
Spacer(modifier = Modifier.height(Spacings.small))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = uiState.password,
onValueChange = {
onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD))
},
label = { Text("Password") },
singleLine = true,
visualTransformation = if (uiState.passwordVisibility) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { onAction(Action.TogglePasswordVisibility) })
)
IconLeadingContent(
icon = Icons.Outlined.Lock,
content = {
ValidatedOutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = uiState.password.value,
errorText = uiState.password.error,
onValueChange = {
onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD))
},
labelText = "Password",
visualTransformation = if (uiState.passwordVisibility) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
onAction(Action.PasswordSignInOrSignUp)
}),
trailingIcon = {
IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) {
val iconId = if (uiState.passwordVisibility) {
DesignR.drawable.ic_visibility
} else {
DesignR.drawable.ic_visibility_off
}
Icon(
painter = painterResource(id = iconId),
contentDescription = if (uiState.passwordVisibility) "Hide password" else "Show password"
)
}
}
)
})
TextButton(
onClick = {
onAction(Action.ForgotPassword)
Expand All @@ -111,14 +162,10 @@ You may login to your existing account or create a new one if you don't have one
Spacer(modifier = Modifier.height(Spacings.medium))
Button(
onClick = {
if (uiState.isAlreadyRegistered) {
onAction(Action.PasswordCredentialSignIn)
} else {
onAction(Action.NavigateToRegister)
}
onAction(Action.PasswordSignInOrSignUp)
},
modifier = Modifier.fillMaxWidth(),
enabled = uiState.email.isNotEmpty() && uiState.password.isNotEmpty()
enabled = uiState.isPasswordSignInEnabled
) {
Text(
text = if (uiState.isAlreadyRegistered) "Login" else "Register"
Expand All @@ -145,11 +192,7 @@ You may login to your existing account or create a new one if you don't have one
Spacer(modifier = Modifier.height(Spacings.medium))
SignInWithGoogleButton(
onButtonClick = {
if (uiState.isAlreadyRegistered) {
onAction(Action.GoogleSignIn)
} else {
onAction(Action.GoogleSignUp)
}
onAction(Action.GoogleSignInOrSignUp)
},
isAlreadyRegistered = uiState.isAlreadyRegistered,
)
Expand All @@ -169,12 +212,12 @@ private fun LoginScreenPreview(
private class LoginScreenPreviewProvider : PreviewParameterProvider<UiState> {
override val values: Sequence<UiState> = sequenceOf(
UiState(
email = "",
password = "",
email = FieldState(""),
password = FieldState(""),
passwordVisibility = false,
), UiState(
email = "[email protected]",
password = "password",
email = FieldState("[email protected]"),
password = FieldState("password"),
passwordVisibility = true,
isAlreadyRegistered = true
)
Expand Down
Loading

0 comments on commit 9c33498

Please sign in to comment.