package com.parseable.android.ui.screens.login import androidx.compose.foundation.layout.* 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.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import com.parseable.android.ui.LocalErrorHandler 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.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen( onLoginSuccess: () -> Unit, sessionExpired: Boolean = false, viewModel: LoginViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() val focusManager = LocalFocusManager.current var passwordVisible by rememberSaveable { mutableStateOf(false) } val errorHandler = LocalErrorHandler.current LaunchedEffect(state.loginSuccess) { if (state.loginSuccess) { onLoginSuccess() } } LaunchedEffect(sessionExpired) { if (sessionExpired) { errorHandler.showError("Session expired. Please log in again.") } } Scaffold { padding -> Column( modifier = Modifier .fillMaxSize() .padding(padding) .verticalScroll(rememberScrollState()) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Text( text = "Parseable", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Log Viewer", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Spacer(modifier = Modifier.height(48.dp)) OutlinedTextField( value = state.serverUrl, onValueChange = viewModel::onServerUrlChange, label = { Text("Server URL") }, placeholder = { Text("https://demo.parseable.com") }, singleLine = true, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next, ), keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Down) }, ), isError = state.serverUrlError != null, supportingText = state.serverUrlError?.let { { Text(it) } }, ) Spacer(modifier = Modifier.height(12.dp)) OutlinedTextField( value = state.username, onValueChange = viewModel::onUsernameChange, label = { Text("Username") }, singleLine = true, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, ), keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Down) }, ), isError = state.usernameError != null, supportingText = state.usernameError?.let { { Text(it) } }, ) Spacer(modifier = Modifier.height(12.dp)) OutlinedTextField( value = state.password, onValueChange = viewModel::onPasswordChange, label = { Text("Password") }, singleLine = true, modifier = Modifier.fillMaxWidth(), visualTransformation = if (passwordVisible) { VisualTransformation.None } else { PasswordVisualTransformation() }, trailingIcon = { IconButton(onClick = { passwordVisible = !passwordVisible }) { Icon( imageVector = if (passwordVisible) { Icons.Filled.VisibilityOff } else { Icons.Filled.Visibility }, contentDescription = if (passwordVisible) "Hide password" else "Show password", ) } }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( onDone = { focusManager.clearFocus() viewModel.onLogin() }, ), isError = state.passwordError != null, supportingText = state.passwordError?.let { { Text(it) } }, ) Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Checkbox( checked = state.allowInsecure, onCheckedChange = viewModel::onAllowInsecureChange, ) Text( text = "Allow insecure (HTTP) connections", style = MaterialTheme.typography.bodyMedium, ) } if (state.allowInsecure) { val isPrivateNetwork = remember(state.serverUrl) { val host = state.serverUrl .removePrefix("http://").removePrefix("https://") .substringBefore("/").substringBefore(":") host.isBlank() || host == "localhost" || host.startsWith("127.") || host.startsWith("10.") || host.startsWith("192.168.") || host.matches(Regex("^172\\.(1[6-9]|2[0-9]|3[01])\\..*")) } if (!isPrivateNetwork) { Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.7f), ), modifier = Modifier.fillMaxWidth(), ) { Text( text = "Warning: HTTP sends credentials and data in plaintext. " + "Only use this for trusted private networks.", modifier = Modifier.padding(12.dp), color = MaterialTheme.colorScheme.onErrorContainer, style = MaterialTheme.typography.bodySmall, ) } } } Spacer(modifier = Modifier.height(24.dp)) Button( onClick = viewModel::onLogin, modifier = Modifier .fillMaxWidth() .defaultMinSize(minHeight = 50.dp), enabled = !state.isLoading, ) { if (state.isLoading) { CircularProgressIndicator( modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary, ) Spacer(modifier = Modifier.width(8.dp)) } Text("Connect") } if (state.hasSavedCredentials && state.password.isEmpty()) { Spacer(modifier = Modifier.height(8.dp)) OutlinedButton( onClick = viewModel::loginWithSavedCredentials, modifier = Modifier .fillMaxWidth() .defaultMinSize(minHeight = 50.dp), enabled = !state.isLoading, ) { Text("Use saved credentials") } } state.error?.let { errorText -> Spacer(modifier = Modifier.height(16.dp)) Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.errorContainer, ), modifier = Modifier.fillMaxWidth(), ) { Text( text = errorText, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onErrorContainer, style = MaterialTheme.typography.bodyMedium, ) } } } } }