From 7f6aa9482a8effa2b7690865d3a9c29f85c56947 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 00:57:30 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20typed-confirm=20on=20AdminUser?= =?UTF-8?q?s=20delete=20(audit=20v3=20=C2=A74.20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete-user was a plain Delete/Cancel AlertDialog — too permissive for an irreversible action. Now mirrors Flutter's TypedConfirmSheet: an OutlinedTextField where the admin must type "DELETE" before the Delete button enables (case-insensitive). The button stays disabled and muted until the word matches. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/admin/ui/AdminUsersScreen.kt | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt index c8d77543..d10e63b5 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt @@ -512,18 +512,41 @@ private fun DeleteConfirmDialog( onDismiss: () -> Unit, onConfirm: () -> Unit, ) { + var typed by remember { mutableStateOf("") } + val confirmed = typed.trim().equals(DELETE_CONFIRM_WORD, ignoreCase = true) AlertDialog( onDismissRequest = onDismiss, title = { Text("Delete user?") }, text = { - Text("Delete @${user.username}? This removes the account and all their data.") + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + "Delete @${user.username}? This removes the account and all " + + "their data. Type $DELETE_CONFIRM_WORD to confirm.", + ) + OutlinedTextField( + value = typed, + onValueChange = { typed = it }, + label = { Text("Type $DELETE_CONFIRM_WORD") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } }, confirmButton = { - TextButton(onClick = onConfirm) { - Text("Delete", color = MaterialTheme.colorScheme.error) + TextButton(onClick = onConfirm, enabled = confirmed) { + Text( + "Delete", + color = if (confirmed) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }, ) } +private const val DELETE_CONFIRM_WORD = "DELETE" +