diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 31cb424..0fa1874 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -16,7 +16,7 @@ android {
minSdk = 26
targetSdk = 35
versionCode = 1
- versionName = "0.0.8"
+ versionName = "0.0.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0ac74ba..055d5fb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,22 +19,11 @@
-
-
-
-
-
-
-
{
_status.value = AuthStatus.Authenticated(authManager.getUsername())
syncManager.sync()
}
is LoginResult.Failure -> {
- _status.value = AuthStatus.Error(result.message)
+ _status.value = AuthStatus.LoginError(result.message)
}
}
}
@@ -76,8 +71,4 @@ class AuthViewModel @Inject constructor(
fun clearError() {
_status.value = AuthStatus.NotAuthenticated
}
-
- fun handleOAuthError(message: String) {
- _status.value = AuthStatus.Error(message)
- }
}
diff --git a/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt b/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt
index bcb8046..353c66a 100644
--- a/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt
+++ b/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt
@@ -1,7 +1,5 @@
package com.planify.mobile.ui.auth
-import android.net.Uri
-import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -10,50 +8,81 @@ 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.layout.size
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+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.Visibility
+import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+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.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
import com.planify.mobile.R
private val MintBackground = Color(0xFFF0FDF4)
private val BonsaiGreen = Color(0xFF38A169)
+private val FieldBorder = Color(0xFFD1D5DB)
+private val LabelColor = Color(0xFF374151)
+private val TitleColor = Color(0xFF111827)
@Composable
-fun LoginScreen(viewModel: AuthViewModel = hiltViewModel()) {
+fun LoginScreen(viewModel: AuthViewModel) {
val status by viewModel.status.collectAsState()
- val context = LocalContext.current
+ val isLoading = status is AuthStatus.Checking
+ val errorMessage = (status as? AuthStatus.LoginError)?.message
+
+ var username by rememberSaveable { mutableStateOf("") }
+ var password by rememberSaveable { mutableStateOf("") }
+ var passwordVisible by remember { mutableStateOf(false) }
+ val passwordFocus = remember { FocusRequester() }
Box(
modifier = Modifier
.fillMaxSize()
- .background(MintBackground),
+ .background(MintBackground)
+ .imePadding(),
contentAlignment = Alignment.Center,
) {
Surface(
shape = RoundedCornerShape(16.dp),
- shadowElevation = 4.dp,
+ shadowElevation = 6.dp,
color = Color.White,
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 32.dp),
+ .padding(horizontal = 28.dp)
+ .verticalScroll(rememberScrollState()),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -65,13 +94,13 @@ fun LoginScreen(viewModel: AuthViewModel = hiltViewModel()) {
modifier = Modifier.size(72.dp),
)
- Spacer(Modifier.height(12.dp))
+ Spacer(Modifier.height(10.dp))
Text(
text = "Bonsai",
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
- color = Color(0xFF111827),
+ color = TitleColor,
)
Spacer(Modifier.height(4.dp))
@@ -80,57 +109,126 @@ fun LoginScreen(viewModel: AuthViewModel = hiltViewModel()) {
text = "Connexion",
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
- color = Color(0xFF374151),
+ color = LabelColor,
)
Spacer(Modifier.height(24.dp))
- when (status) {
- is AuthStatus.Checking -> {
- CircularProgressIndicator(color = BonsaiGreen)
- Spacer(Modifier.height(8.dp))
- Text(
- text = "Vérification…",
- style = MaterialTheme.typography.bodySmall,
- color = Color(0xFF6B7280),
- )
- }
+ // Username field
+ Text(
+ text = "Email ou nom d'utilisateur",
+ style = MaterialTheme.typography.bodySmall,
+ color = LabelColor,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 4.dp),
+ )
+ OutlinedTextField(
+ value = username,
+ onValueChange = { username = it },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ enabled = !isLoading,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Email,
+ imeAction = ImeAction.Next,
+ ),
+ keyboardActions = KeyboardActions(
+ onNext = { passwordFocus.requestFocus() }
+ ),
+ colors = OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = FieldBorder,
+ focusedBorderColor = BonsaiGreen,
+ ),
+ shape = RoundedCornerShape(6.dp),
+ )
- is AuthStatus.Error -> {
- Text(
- text = (status as AuthStatus.Error).message,
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(bottom = 12.dp),
- )
- LoginButton {
- val url = viewModel.buildAuthUrl()
- CustomTabsIntent.Builder().setShowTitle(true).build()
- .launchUrl(context, Uri.parse(url))
- }
- }
+ Spacer(Modifier.height(12.dp))
- else -> {
- LoginButton {
- val url = viewModel.buildAuthUrl()
- CustomTabsIntent.Builder().setShowTitle(true).build()
- .launchUrl(context, Uri.parse(url))
+ // Password field
+ Text(
+ text = "Mot de passe",
+ style = MaterialTheme.typography.bodySmall,
+ color = LabelColor,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 4.dp),
+ )
+ OutlinedTextField(
+ value = password,
+ onValueChange = { password = it },
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(passwordFocus),
+ singleLine = true,
+ enabled = !isLoading,
+ visualTransformation = if (passwordVisible) VisualTransformation.None
+ else PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done,
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ if (username.isNotBlank() && password.isNotBlank()) {
+ viewModel.login(username.trim(), password)
+ }
}
+ ),
+ trailingIcon = {
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(
+ imageVector = if (passwordVisible) Icons.Outlined.Visibility
+ else Icons.Outlined.VisibilityOff,
+ contentDescription = if (passwordVisible) "Masquer" else "Afficher",
+ tint = Color(0xFF6B7280),
+ )
+ }
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = FieldBorder,
+ focusedBorderColor = BonsaiGreen,
+ ),
+ shape = RoundedCornerShape(6.dp),
+ )
+
+ errorMessage?.let {
+ Spacer(Modifier.height(8.dp))
+ Text(
+ text = it,
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+
+ Spacer(Modifier.height(20.dp))
+
+ Button(
+ onClick = { viewModel.login(username.trim(), password) },
+ enabled = !isLoading && username.isNotBlank() && password.isNotBlank(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = BonsaiGreen),
+ shape = RoundedCornerShape(6.dp),
+ ) {
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(20.dp),
+ color = Color.White,
+ strokeWidth = 2.dp,
+ )
+ } else {
+ Text(
+ text = "Se connecter",
+ color = Color.White,
+ fontWeight = FontWeight.Medium,
+ fontSize = 15.sp,
+ )
}
}
}
}
}
}
-
-@Composable
-private fun LoginButton(onClick: () -> Unit) {
- Button(
- onClick = onClick,
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(containerColor = BonsaiGreen),
- shape = RoundedCornerShape(8.dp),
- ) {
- Text("Se connecter", color = Color.White, fontWeight = FontWeight.Medium)
- }
-}