diff --git a/android/app/src/main/java/net/linkedcorp/linkedsign/ui/LinkedSignApp.kt b/android/app/src/main/java/net/linkedcorp/linkedsign/ui/LinkedSignApp.kt index be71c1c..67c5236 100644 --- a/android/app/src/main/java/net/linkedcorp/linkedsign/ui/LinkedSignApp.kt +++ b/android/app/src/main/java/net/linkedcorp/linkedsign/ui/LinkedSignApp.kt @@ -161,8 +161,8 @@ private val CardGlow = Brush.linearGradient( private const val SignatureHeight = 60 -private enum class AuthMode { Login, Register, VerifyNotice } -private enum class AppTab(val title: String) { Dashboard("Inicio"), Documents("Documentos"), Sign("Firmar"), Certificates("Certificados"), Profile("Perfil") } +private enum class AuthMode { Login } +private enum class AppTab(val title: String) { Documents("Documentos"), Sign("Firmar"), Certificates("Certificados") } private data class PdfPreviewState( val file: File, @@ -200,7 +200,7 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo val state = viewModel.uiState var authMode by rememberSaveable { mutableStateOf(AuthMode.Login) } - var selectedTab by rememberSaveable { mutableStateOf(AppTab.Dashboard) } + var selectedTab by rememberSaveable { mutableStateOf(AppTab.Documents) } var pendingSignDocumentId by rememberSaveable { mutableStateOf(null) } val biometricAvailable = remember(activity) { canUseBiometricAccess(activity) } var appUnlocked by rememberSaveable { mutableStateOf(false) } @@ -213,12 +213,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo viewModel.bootstrap() } - LaunchedEffect(state.emailForVerificationNotice) { - if (!state.emailForVerificationNotice.isNullOrBlank()) { - authMode = AuthMode.VerifyNotice - } - } - LaunchedEffect(state.loading, state.authenticated, biometricAvailable) { if (!state.loading && !bootstrapGateHandled) { bootstrapGateHandled = true @@ -252,6 +246,19 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo } } }, + actions = { + IconButton(onClick = { + scope.launch { + viewModel.logout() + authMode = AuthMode.Login + pendingSignDocumentId = null + appUnlocked = !(biometricAvailable && state.biometricEnabled && state.biometricHasCredentials) + snackbarHostState.showSnackbar("Sesión cerrada.") + } + }) { + Icon(Icons.Outlined.Logout, contentDescription = "Cerrar sesión", tint = Color.White) + } + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Brand, titleContentColor = Color.White @@ -266,11 +273,9 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo tonalElevation = 0.dp ) { val tabs = listOf( - AppTab.Dashboard to Icons.Outlined.Dashboard, AppTab.Documents to Icons.Outlined.Description, AppTab.Sign to Icons.Outlined.Draw, - AppTab.Certificates to Icons.Outlined.Badge, - AppTab.Profile to Icons.Outlined.AccountCircle + AppTab.Certificates to Icons.Outlined.Badge ) tabs.forEach { (tab, icon) -> NavigationBarItem( @@ -308,7 +313,7 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo scope.launch { runCatching { viewModel.biometricLogin() } .onSuccess { - selectedTab = AppTab.Dashboard + selectedTab = AppTab.Documents appUnlocked = true snackbarHostState.showSnackbar("Sesión iniciada con huella.") } @@ -343,15 +348,11 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo } ) !state.authenticated -> AuthFlow( - mode = authMode, - onModeChange = { authMode = it }, - verifyEmail = state.emailForVerificationNotice, - onConsumeVerifyNotice = { viewModel.clearVerificationNotice() }, onLogin = { email, password -> scope.launch { runCatching { viewModel.login(email, password) } .onSuccess { - selectedTab = AppTab.Dashboard + selectedTab = AppTab.Documents appUnlocked = true pendingBiometricEmail = email.trim() pendingBiometricPassword = password @@ -368,17 +369,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo } } }, - onRegister = { email, password, certPassword, certificateUri, certificateName -> - scope.launch { - runCatching { viewModel.register(email, password, certPassword, certificateUri, certificateName) } - .onSuccess { - authMode = AuthMode.VerifyNotice - } - .onFailure { error -> - snackbarHostState.showSnackbar((error as? AppException)?.message ?: "No se pudo registrar.") - } - } - }, modifier = Modifier.padding(padding) ) else -> AppFlow( @@ -434,22 +424,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo .onFailure { snackbarHostState.showSnackbar((it as? AppException)?.message ?: "No se pudo firmar el documento.") } } }, - onRequestPasswordChange = { currentPassword, newPassword -> - scope.launch { - runCatching { viewModel.requestPasswordChange(currentPassword, newPassword) } - .onSuccess { snackbarHostState.showSnackbar("Te enviamos un correo para confirmar el cambio de contraseña.") } - .onFailure { snackbarHostState.showSnackbar((it as? AppException)?.message ?: "No se pudo solicitar el cambio.") } - } - }, - onLogout = { - scope.launch { - viewModel.logout() - authMode = AuthMode.Login - pendingSignDocumentId = null - appUnlocked = !(biometricAvailable && state.biometricEnabled && state.biometricHasCredentials) - snackbarHostState.showSnackbar("Sesión cerrada.") - } - }, selectedSignDocumentId = pendingSignDocumentId, onSelectDocumentToSign = { documentId -> pendingSignDocumentId = documentId.toString() @@ -524,12 +498,7 @@ private fun LoadingState() { @Composable private fun AuthFlow( - mode: AuthMode, - onModeChange: (AuthMode) -> Unit, - verifyEmail: String?, - onConsumeVerifyNotice: () -> Unit, onLogin: (String, String) -> Unit, - onRegister: (String, String, String, Uri, String) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -554,7 +523,7 @@ private fun AuthFlow( } Text("LinkedSign", color = Color.White, fontSize = AppFontSizes.brand, fontWeight = FontWeight.Bold) Text( - if (mode == AuthMode.Register) "Crear cuenta" else "Firma digital profesional", + "Firmador móvil", color = Color.White.copy(alpha = 0.75f), fontSize = AppFontSizes.base ) @@ -569,23 +538,7 @@ private fun AuthFlow( .padding(horizontal = 20.dp, vertical = 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - when (mode) { - AuthMode.Login -> LoginCard( - onSubmit = onLogin, - onGoRegister = { onModeChange(AuthMode.Register) } - ) - AuthMode.Register -> RegisterCard( - onSubmit = onRegister, - onGoLogin = { onModeChange(AuthMode.Login) } - ) - AuthMode.VerifyNotice -> VerifyNoticeCard( - email = verifyEmail, - onBackToLogin = { - onConsumeVerifyNotice() - onModeChange(AuthMode.Login) - } - ) - } + LoginCard(onSubmit = onLogin) if (BuildConfig.DEBUG) { Text( "Servidor: ${BuildConfig.API_BASE_URL}", @@ -682,8 +635,6 @@ private fun AppFlow( onUploadCertificate: (Uri, String, String, String) -> Unit, onDeleteDocument: (UnifiedDocumentItem) -> Unit, onSign: (Int, Int, String, String, String, Int, SignatureRect?) -> Unit, - onRequestPasswordChange: (String, String) -> Unit, - onLogout: () -> Unit, onSelectDocumentToSign: (Int) -> Unit, accessToken: () -> String? ) { @@ -693,7 +644,6 @@ private fun AppFlow( .padding(padding) ) { when (selectedTab) { - AppTab.Dashboard -> DashboardScreen(state = state, onSync = onSync) AppTab.Documents -> DocumentsScreen( state = state, accessToken = accessToken, @@ -711,13 +661,12 @@ private fun AppFlow( onSign = onSign ) AppTab.Certificates -> CertificatesScreen(state = state, onSync = onSync, onUploadCertificate = onUploadCertificate) - AppTab.Profile -> ProfileScreen(state = state, onRequestPasswordChange = onRequestPasswordChange, onLogout = onLogout, onSync = onSync) } } } @Composable -private fun LoginCard(onSubmit: (String, String) -> Unit, onGoRegister: () -> Unit) { +private fun LoginCard(onSubmit: (String, String) -> Unit) { var email by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } @@ -750,7 +699,7 @@ private fun LoginCard(onSubmit: (String, String) -> Unit, onGoRegister: () -> Un } Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text("Iniciar sesión", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = BrandDark) - Text("Accede a tu cuenta LinkedSign", color = TextMuted, style = MaterialTheme.typography.bodySmall) + Text("Accede con tu cuenta existente. El registro se realiza desde la web.", color = TextMuted, style = MaterialTheme.typography.bodySmall) } AppTextField(value = email, onValueChange = { email = it }, label = "Correo electrónico", keyboardType = KeyboardType.Email) AppTextField(value = password, onValueChange = { password = it }, label = "Contraseña", isPassword = true) @@ -765,180 +714,17 @@ private fun LoginCard(onSubmit: (String, String) -> Unit, onGoRegister: () -> Un Spacer(Modifier.width(8.dp)) Text("Entrar", fontWeight = FontWeight.SemiBold, fontSize = AppFontSizes.xl) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text("¿No tienes cuenta?", color = Color(0xFF6B7280), fontSize = AppFontSizes.base) - TextButton(onClick = onGoRegister, contentPadding = PaddingValues(horizontal = 6.dp)) { - Text("Regístrate", color = Brand, fontWeight = FontWeight.SemiBold, fontSize = AppFontSizes.base) - } - } - } - } -} - -@Composable -private fun RegisterCard(onSubmit: (String, String, String, Uri, String) -> Unit, onGoLogin: () -> Unit) { - val context = LocalContext.current - var email by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } - var confirmPassword by rememberSaveable { mutableStateOf("") } - var certPassword by rememberSaveable { mutableStateOf("") } - var consentAccepted by rememberSaveable { mutableStateOf(false) } - var certificateUri by rememberSaveable { mutableStateOf(null) } - var certificateName by rememberSaveable { mutableStateOf("") } - val passwordProblems = passwordIssues(password) - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - uri ?: return@rememberLauncherForActivityResult - try { - context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - } catch (_: Exception) { - } - certificateUri = uri - certificateName = queryFileName(context, uri) ?: "certificado.p12" - } - - ElevatedCard( - shape = RoundedCornerShape(24.dp), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 8.dp), - colors = CardDefaults.elevatedCardColors(containerColor = Color.White) - ) { - Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { Surface( color = SurfaceAlt, - shape = RoundedCornerShape(16.dp), - border = BorderStroke(1.dp, BrandLight) + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, StrokeSoft) ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - Box( - modifier = Modifier.size(32.dp).background(Color.White, RoundedCornerShape(12.dp)), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Outlined.Verified, contentDescription = null, tint = Brand, modifier = Modifier.size(18.dp)) - } - Column { - Text("Alta protegida", color = BrandDark, fontWeight = FontWeight.SemiBold, fontSize = AppFontSizes.base) - Text("Correo válido, clave fuerte y certificado para el consentimiento legal", color = TextMuted, fontSize = AppFontSizes.sm) - } - } - } - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text("Crear cuenta", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = BrandDark) - Text("Registra tu cuenta en LinkedSign", color = TextMuted, style = MaterialTheme.typography.bodySmall) - } - AppTextField(value = email, onValueChange = { email = it }, label = "Correo electrónico", keyboardType = KeyboardType.Email) - AppTextField(value = password, onValueChange = { password = it }, label = "Contraseña", isPassword = true) - PasswordChecklist(password = password) - AppTextField(value = confirmPassword, onValueChange = { confirmPassword = it }, label = "Confirmar contraseña", isPassword = true) - AppTextField(value = certPassword, onValueChange = { certPassword = it }, label = "Contraseña del certificado", isPassword = true) - Surface(color = SurfaceAlt, shape = RoundedCornerShape(14.dp), border = BorderStroke(1.dp, StrokeSoft)) { - Column(modifier = Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Registro legal inicial", color = BrandDark, fontWeight = FontWeight.SemiBold, fontSize = AppFontSizes.base) - Text( - "Debes cargar tu P12/PFX. La contraseña del certificado solo se usará para firmar los documentos legales del primer registro y no se guardará.", - color = TextMuted, - fontSize = AppFontSizes.sm - ) - Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { - OutlinedActionButton( - label = if (certificateName.isBlank()) "Seleccionar P12/PFX" else certificateName, - icon = Icons.Outlined.Badge, - modifier = Modifier.weight(1f) - ) { - launcher.launch(arrayOf("*/*")) - } - } - Row(verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(10.dp)) { - androidx.compose.material3.Checkbox( - checked = consentAccepted, - onCheckedChange = { consentAccepted = it } - ) - Text( - "Acepto los términos de uso, la política de privacidad y el consentimiento expreso para tratamiento de datos y uso de firma electrónica.", - color = TextMuted, - fontSize = AppFontSizes.sm, - modifier = Modifier.weight(1f) - ) - } - } - } - if (passwordProblems.isNotEmpty() && password.isNotBlank()) { - Surface(color = WarningBg, shape = RoundedCornerShape(10.dp)) { - Text("La contraseña aún no cumple todos los requisitos.", color = Warning, modifier = Modifier.padding(10.dp), fontSize = AppFontSizes.md) - } - } - if (password != confirmPassword && confirmPassword.isNotBlank()) { - Surface(color = Color(0xFFFEE2E2), shape = RoundedCornerShape(10.dp)) { - Text("Las contraseñas no coinciden.", color = Danger, modifier = Modifier.padding(10.dp), fontSize = AppFontSizes.md) - } - } - Spacer(Modifier.height(2.dp)) - Button( - onClick = { - when { - passwordProblems.isNotEmpty() -> Unit - password != confirmPassword -> Unit - certPassword.isBlank() -> Unit - certificateUri == null -> Unit - !consentAccepted -> Unit - else -> onSubmit(email, password, certPassword, certificateUri!!, certificateName.ifBlank { "certificado.p12" }) - } - }, - modifier = Modifier.fillMaxWidth().height(50.dp), - shape = RoundedCornerShape(14.dp), - colors = ButtonDefaults.buttonColors(containerColor = Brand) - ) { - Text("Crear cuenta", fontWeight = FontWeight.SemiBold, fontSize = AppFontSizes.xl) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text("¿Ya tienes cuenta?", color = Color(0xFF6B7280), fontSize = AppFontSizes.base) - TextButton(onClick = onGoLogin, contentPadding = PaddingValues(horizontal = 6.dp)) { - Text("Inicia sesión", color = Brand, fontWeight = FontWeight.SemiBold, fontSize = AppFontSizes.base) - } - } - } - } -} - -@Composable -private fun VerifyNoticeCard(email: String?, onBackToLogin: () -> Unit) { - ElevatedCard( - shape = RoundedCornerShape(24.dp), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 8.dp), - colors = CardDefaults.elevatedCardColors(containerColor = Color.White) - ) { - Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .size(64.dp) - .background(Brush.radialGradient(listOf(SuccessBg, Color.White)), CircleShape), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Outlined.CheckCircle, contentDescription = null, tint = Success, modifier = Modifier.size(32.dp)) - } - Text("Confirma tu cuenta", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = BrandDark) - Text( - "Enviamos un enlace de verificación a ${email ?: "tu correo"}. Debes confirmarlo antes de iniciar sesión.", - color = TextMuted, - style = MaterialTheme.typography.bodyMedium - ) - Button( - onClick = onBackToLogin, - modifier = Modifier.fillMaxWidth().height(50.dp), - shape = RoundedCornerShape(14.dp), - colors = ButtonDefaults.buttonColors(containerColor = Brand) - ) { - Text("Volver al inicio", fontWeight = FontWeight.SemiBold) + Text( + "La app móvil se usa para firmar y revisar documentos. Para crear cuenta o gestionar accesos, usa la versión web.", + color = TextMuted, + fontSize = AppFontSizes.sm, + modifier = Modifier.padding(12.dp) + ) } } }