diff --git a/app/src/main/java/com/flux/data/dao/WorkspaceDao.kt b/app/src/main/java/com/flux/data/dao/WorkspaceDao.kt index 64c0c58..7b06505 100644 --- a/app/src/main/java/com/flux/data/dao/WorkspaceDao.kt +++ b/app/src/main/java/com/flux/data/dao/WorkspaceDao.kt @@ -19,7 +19,7 @@ interface WorkspaceDao { @Insert(onConflict=OnConflictStrategy.REPLACE) suspend fun upsertWorkspaces(spaces: List) - @Query("SELECT workspaceId FROM WorkspaceModel WHERE passKey IS NULL OR passKey = ''") + @Query("SELECT workspaceId FROM WorkspaceModel WHERE passKeyHash IS NULL OR passKeyHash = ''") fun observePublicWorkspaceIds(): Flow> @Delete diff --git a/app/src/main/java/com/flux/data/database/FluxDatabase.kt b/app/src/main/java/com/flux/data/database/FluxDatabase.kt index d3271b3..710377d 100644 --- a/app/src/main/java/com/flux/data/database/FluxDatabase.kt +++ b/app/src/main/java/com/flux/data/database/FluxDatabase.kt @@ -1,5 +1,7 @@ package com.flux.data.database +import android.database.Cursor +import android.util.Log import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters @@ -30,6 +32,7 @@ import com.flux.data.model.SettingsModel import com.flux.data.model.TodoInstance import com.flux.data.model.TodoModel import com.flux.data.model.WorkspaceModel +import com.flux.other.PasswordHasher import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.serialization.json.Json @@ -37,7 +40,7 @@ import java.util.UUID @Database( entities = [EventModel::class, LabelModel::class, EventInstanceModel::class, SettingsModel::class, NotesModel::class, HabitModel::class, HabitInstanceModel::class, WorkspaceModel::class, TodoModel::class, JournalModel::class, ProgressBoardModel::class, TodoInstance::class], - version = 11, + version = 12, exportSchema = false ) @TypeConverters(Converter::class) @@ -405,4 +408,118 @@ val MIGRATION_10_11 = object : Migration(10, 11) { "ON `HabitInstanceModel` (`instanceDate`)" ) } +} + +val MIGRATION_11_12 = object : Migration(11, 12) { + + private val TAG = "Migration_11_12" + private val OLD_TABLE = "WorkspaceModel" + private val TMP_TABLE = "WorkspaceModel_new" + + override fun migrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "Starting migration 11 -> 12 (passKey -> hashed passKeyHash)") + + recreateTableWithHashedColumn(db) + rehashExistingPasswords(db) + + Log.i(TAG, "Migration 11 -> 12 finished") + } + + /** Step 1: recreate WorkspaceModel with passKeyHash instead of passKey, copying all rows as-is. */ + private fun recreateTableWithHashedColumn(db: SupportSQLiteDatabase) { + db.safeExec( + """ + CREATE TABLE IF NOT EXISTS `$TMP_TABLE` ( + `workspaceId` TEXT NOT NULL PRIMARY KEY, + `title` TEXT NOT NULL, + `description` TEXT NOT NULL, + `colorInd` INTEGER NOT NULL, + `cover` TEXT NOT NULL, + `icon` INTEGER NOT NULL, + `passKeyHash` TEXT, + `isPinned` INTEGER NOT NULL, + `selectedSpaces` TEXT NOT NULL + ) + """.trimIndent() + ) + + db.safeExec( + """ + INSERT INTO `$TMP_TABLE` + (`workspaceId`, `title`, `description`, `colorInd`, `cover`, `icon`, `passKeyHash`, `isPinned`, `selectedSpaces`) + SELECT + `workspaceId`, `title`, `description`, `colorInd`, `cover`, `icon`, `passKey`, `isPinned`, `selectedSpaces` + FROM `$OLD_TABLE` + """.trimIndent() + ) + + db.safeExec("DROP TABLE IF EXISTS `$OLD_TABLE`") + db.safeExec("ALTER TABLE `$TMP_TABLE` RENAME TO `$OLD_TABLE`") + + Log.d(TAG, "WorkspaceModel table recreated with passKeyHash column") + } + + /** + * Step 2: hash every non-blank plaintext passKeyHash value in place. + * Row-level failures are isolated so one bad row can never crash the migration + * or the app — this is a hard requirement, not an optimization. + */ + private fun rehashExistingPasswords(db: SupportSQLiteDatabase) { + var total = 0 + var migrated = 0 + var alreadyHashed = 0 + var failed = 0 + + var cursor: Cursor? = null + try { + cursor = db.query( + "SELECT `workspaceId`, `passKeyHash` FROM `$OLD_TABLE` " + + "WHERE `passKeyHash` IS NOT NULL AND TRIM(`passKeyHash`) != ''" + ) + + val idColumn = cursor.getColumnIndexOrThrow("workspaceId") + val passColumn = cursor.getColumnIndexOrThrow("passKeyHash") + + while (cursor.moveToNext()) { + total++ + val workspaceId = cursor.getString(idColumn) + val currentValue = cursor.getString(passColumn) + + try { + if (PasswordHasher.isHashed(currentValue)) { + Log.d(TAG, "Workspace $workspaceId already hashed, skipping") + alreadyHashed++ + continue + } + + val hashed = PasswordHasher.hash(currentValue) + + // Use safeExec-equivalent guarded update; bind values manually + // since safeExec (per your codebase) likely takes raw SQL only. + db.execSQL( + "UPDATE `$OLD_TABLE` SET `passKeyHash` = ? WHERE `workspaceId` = ?", + arrayOf(hashed, workspaceId) + ) + migrated++ + Log.d(TAG, "Workspace $workspaceId passkey hashed successfully") + } catch (rowError: Exception) { + failed++ + Log.e( + TAG, + "Failed to hash passkey for workspace $workspaceId, leaving value untouched", + rowError + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "Critical error while iterating WorkspaceModel rows for hashing", e) + } finally { + cursor?.close() + Log.i( + TAG, + "Passkey hashing summary — total: $total, migrated: $migrated, " + + "alreadyHashed: $alreadyHashed, failed: $failed" + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/flux/data/model/WorkspaceModel.kt b/app/src/main/java/com/flux/data/model/WorkspaceModel.kt index 7f4fe7c..f8ee21d 100644 --- a/app/src/main/java/com/flux/data/model/WorkspaceModel.kt +++ b/app/src/main/java/com/flux/data/model/WorkspaceModel.kt @@ -16,6 +16,7 @@ import androidx.room.PrimaryKey import java.util.UUID import com.flux.R import kotlinx.serialization.Serializable +import com.flux.other.PasswordHasher @Serializable @Entity @@ -27,10 +28,14 @@ data class WorkspaceModel( val colorInd: Int = 0, val cover: String = "", val icon: Int = 48, - val passKey: String? = null, + val passKeyHash: String? = null, // renamed from passKey; now stores a PBKDF2 hash, never plaintext val isPinned: Boolean = false, val selectedSpaces: List = emptyList() -) +) { + /** True if this workspace currently requires a passkey to unlock. */ + val isLocked: Boolean + get() = !passKeyHash.isNullOrBlank() +} data class Space( val id: Int, @@ -49,4 +54,18 @@ fun getSpacesList(): List { Space(6, stringResource(R.string.Analytics), Icons.Default.Analytics), Space(7, stringResource(R.string.progress_tracker), Icons.Default.TrackChanges) ) +} + +fun WorkspaceModel.lockWith(rawPassword: String): WorkspaceModel { + require(rawPassword.isNotBlank()) { "Passkey cannot be blank" } + return copy(passKeyHash = PasswordHasher.hash(rawPassword)) +} + +/** Removes the passkey, unlocking the workspace permanently until re-locked. */ +fun WorkspaceModel.removePasskey(): WorkspaceModel = copy(passKeyHash = null) + +/** Checks [rawPassword] against the stored hash. Returns false if workspace isn't locked. */ +fun WorkspaceModel.verifyPasskey(rawPassword: String): Boolean { + val stored = passKeyHash ?: return false + return PasswordHasher.verify(rawPassword, stored) } \ No newline at end of file diff --git a/app/src/main/java/com/flux/di/DataModule.kt b/app/src/main/java/com/flux/di/DataModule.kt index c8145bf..4c981ac 100644 --- a/app/src/main/java/com/flux/di/DataModule.kt +++ b/app/src/main/java/com/flux/di/DataModule.kt @@ -17,6 +17,7 @@ import com.flux.data.dao.TodoInstanceDao import com.flux.data.dao.WorkspaceDao import com.flux.data.database.FluxDatabase import com.flux.data.database.MIGRATION_10_11 +import com.flux.data.database.MIGRATION_11_12 import com.flux.data.database.MIGRATION_1_2 import com.flux.data.database.MIGRATION_2_3 import com.flux.data.database.MIGRATION_3_4 @@ -55,7 +56,8 @@ object DataModule { MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, - MIGRATION_10_11 + MIGRATION_10_11, + MIGRATION_11_12 ) .build() diff --git a/app/src/main/java/com/flux/other/Security.kt b/app/src/main/java/com/flux/other/Security.kt new file mode 100644 index 0000000..dee372b --- /dev/null +++ b/app/src/main/java/com/flux/other/Security.kt @@ -0,0 +1,69 @@ +package com.flux.other + +import android.util.Base64 +import java.security.SecureRandom +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec +import android.util.Log + +/** + * Handles one-way password hashing for workspace passkeys. + * Stored format: "$$" + */ +object PasswordHasher { + + private const val TAG = "PasswordHasher" + private const val ALGORITHM = "PBKDF2WithHmacSHA256" + private const val ITERATIONS = 10_000 + private const val KEY_LENGTH_BITS = 256 + private const val SALT_LENGTH_BYTES = 16 + private const val DELIMITER = "$" + + /** Hashes [rawPassword] with a freshly generated salt. */ + fun hash(rawPassword: String): String { + val salt = ByteArray(SALT_LENGTH_BYTES).apply { SecureRandom().nextBytes(this) } + val hashBytes = pbkdf2(rawPassword.toCharArray(), salt, ITERATIONS, KEY_LENGTH_BITS) + + val encodedSalt = Base64.encodeToString(salt, Base64.NO_WRAP) + val encodedHash = Base64.encodeToString(hashBytes, Base64.NO_WRAP) + + return "$ITERATIONS$DELIMITER$encodedSalt$DELIMITER$encodedHash" + } + + /** Verifies [rawPassword] against a previously [hash]ed value. */ + fun verify(rawPassword: String, stored: String): Boolean { + return try { + val (iterations, salt, expectedHash) = parse(stored) ?: return false + val actualHash = pbkdf2(rawPassword.toCharArray(), salt, iterations, expectedHash.size * 8) + actualHash.contentEquals(expectedHash) + } catch (e: Exception) { + Log.e(TAG, "verify: unable to verify password", e) + false + } + } + + /** True if [value] is already in our hashed format (used to make migration idempotent). */ + fun isHashed(value: String?): Boolean { + if (value.isNullOrBlank()) return false + return parse(value) != null + } + + private fun parse(stored: String): Triple? { + val parts = stored.split(DELIMITER) + if (parts.size != 3) return null + val iterations = parts[0].toIntOrNull() ?: return null + return try { + val salt = Base64.decode(parts[1], Base64.NO_WRAP) + val hash = Base64.decode(parts[2], Base64.NO_WRAP) + Triple(iterations, salt, hash) + } catch (_: IllegalArgumentException) { + null + } + } + + private fun pbkdf2(password: CharArray, salt: ByteArray, iterations: Int, keyLengthBits: Int): ByteArray { + val spec = PBEKeySpec(password, salt, iterations, keyLengthBits) + val factory = SecretKeyFactory.getInstance(ALGORITHM) + return factory.generateSecret(spec).encoded + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flux/ui/screens/analytics/AnalyticsScreen.kt b/app/src/main/java/com/flux/ui/screens/analytics/AnalyticsScreen.kt index 5f39c17..26cb820 100644 --- a/app/src/main/java/com/flux/ui/screens/analytics/AnalyticsScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/analytics/AnalyticsScreen.kt @@ -113,7 +113,7 @@ fun AnalyticScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/events/EventScreen.kt b/app/src/main/java/com/flux/ui/screens/events/EventScreen.kt index 50e4390..7ad1bef 100644 --- a/app/src/main/java/com/flux/ui/screens/events/EventScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/events/EventScreen.kt @@ -123,7 +123,7 @@ fun EventScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/habits/HabitScreen.kt b/app/src/main/java/com/flux/ui/screens/habits/HabitScreen.kt index 06556ca..b02497f 100644 --- a/app/src/main/java/com/flux/ui/screens/habits/HabitScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/habits/HabitScreen.kt @@ -99,7 +99,7 @@ fun HabitScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/journal/JournalScreen.kt b/app/src/main/java/com/flux/ui/screens/journal/JournalScreen.kt index 70b5c91..14f8832 100644 --- a/app/src/main/java/com/flux/ui/screens/journal/JournalScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/journal/JournalScreen.kt @@ -203,7 +203,7 @@ fun JournalScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/notes/NotesScreen.kt b/app/src/main/java/com/flux/ui/screens/notes/NotesScreen.kt index 08210b1..f7742c7 100644 --- a/app/src/main/java/com/flux/ui/screens/notes/NotesScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/notes/NotesScreen.kt @@ -212,7 +212,7 @@ fun NotesScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/progressBoard/ProgressBoard.kt b/app/src/main/java/com/flux/ui/screens/progressBoard/ProgressBoard.kt index 96a94e9..da7c777 100644 --- a/app/src/main/java/com/flux/ui/screens/progressBoard/ProgressBoard.kt +++ b/app/src/main/java/com/flux/ui/screens/progressBoard/ProgressBoard.kt @@ -91,7 +91,7 @@ fun ProgressTrackerScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/search/SearchScreen.kt b/app/src/main/java/com/flux/ui/screens/search/SearchScreen.kt index e61b881..782fc6d 100644 --- a/app/src/main/java/com/flux/ui/screens/search/SearchScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/search/SearchScreen.kt @@ -120,7 +120,7 @@ fun SearchScreen(navController: NavController, states: States, viewModels: ViewM val context = LocalContext.current var query by rememberSaveable { mutableStateOf("") } val allSpaces = getSpacesList().filter { it.id!=6 } - val lockedWorkspace = states.workspaceState.allWorkspaces.filter { it.passKey?.isNotBlank()==true }.map { it.workspaceId } + val lockedWorkspace = states.workspaceState.allWorkspaces.filter { it.isLocked }.map { it.workspaceId } var filterState by remember { mutableStateOf( FilterState( diff --git a/app/src/main/java/com/flux/ui/screens/todo/TodoScreen.kt b/app/src/main/java/com/flux/ui/screens/todo/TodoScreen.kt index da14c37..a5d3733 100644 --- a/app/src/main/java/com/flux/ui/screens/todo/TodoScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/todo/TodoScreen.kt @@ -87,7 +87,7 @@ fun TodoScreen( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/workspaces/EmptyWorkspace.kt b/app/src/main/java/com/flux/ui/screens/workspaces/EmptyWorkspace.kt index 2faaeb2..8674748 100644 --- a/app/src/main/java/com/flux/ui/screens/workspaces/EmptyWorkspace.kt +++ b/app/src/main/java/com/flux/ui/screens/workspaces/EmptyWorkspace.kt @@ -50,7 +50,7 @@ fun EmptyWorkspace( description = workspace.description, cover = workspace.cover, icon = workspace.icon, - isLocked = workspace.passKey!=null, + isLocked = workspace.isLocked, onBackPressed = { navController.popBackStack() }, onAddCover = onAddCover, onRemoveCover = onRemoveCover, diff --git a/app/src/main/java/com/flux/ui/screens/workspaces/NewWorkspaceScreen.kt b/app/src/main/java/com/flux/ui/screens/workspaces/NewWorkspaceScreen.kt index 38ff1ef..687466e 100644 --- a/app/src/main/java/com/flux/ui/screens/workspaces/NewWorkspaceScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/workspaces/NewWorkspaceScreen.kt @@ -48,6 +48,8 @@ import androidx.navigation.NavController import com.flux.R import com.flux.data.model.WorkspaceModel import com.flux.data.model.getSpacesList +import com.flux.data.model.lockWith +import com.flux.data.model.removePasskey import com.flux.other.icons import com.flux.ui.common.ChangeIconSheet import com.flux.ui.common.CompactCard @@ -85,7 +87,9 @@ fun NewWorkspaceScreen( var title by remember { mutableStateOf(workspace.title) } var description by remember { mutableStateOf(workspace.description) } var icon by remember { mutableIntStateOf(workspace.icon) } - var passkey by remember { mutableStateOf(workspace.passKey) } + var isLocked by remember { mutableStateOf(workspace.passKeyHash != null) } + var pendingPasskey by remember { mutableStateOf(null) } + val focusRequesterDesc = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -109,17 +113,20 @@ fun NewWorkspaceScreen( } fun saveWorkspace() { - onEvent( - WorkspaceEvents.UpsertSpace( - workspace.copy( - title = title, - description = description, - icon = icon, - passKey = passkey, - selectedSpaces = selectedSpacesId.toList() - ) - ) + val baseWorkspace = workspace.copy( + title = title, + description = description, + icon = icon, + selectedSpaces = selectedSpacesId.toList() ) + + val resolvedWorkspace = when { + !isLocked -> baseWorkspace.removePasskey() + !pendingPasskey.isNullOrBlank() -> baseWorkspace.lockWith(pendingPasskey!!) + else -> baseWorkspace // keep existing passKeyHash as-is (unchanged) + } + + onEvent(WorkspaceEvents.UpsertSpace(resolvedWorkspace)) } if (showSpaceDeleteWarningDialog) { @@ -205,17 +212,26 @@ fun NewWorkspaceScreen( .fillMaxWidth() .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween){ Text(stringResource(R.string.Lock_Workspace), style = MaterialTheme.typography.bodyLarge) - Switch(passkey!=null, onCheckedChange = { passkey = if(it) "" else null }) + Switch( + checked = isLocked, + onCheckedChange = { checked -> + isLocked = checked + pendingPasskey = if (checked) { "" } else { null } + } + ) } - passkey?.let { + if (isLocked) { Row(Modifier .fillMaxWidth() .padding(8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween){ Text(stringResource(R.string.passkey), style = MaterialTheme.typography.bodyLarge) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - if(it.isNotBlank()) Text("****") + // Show masked dots if either an existing hash is already stored + // OR the user has typed a new passkey in this session. + val hasSetPasskey = workspace.passKeyHash != null || !pendingPasskey.isNullOrBlank() + if (hasSetPasskey) Text("****") IconButton( { isDialogVisible = true }, colors = IconButtonDefaults.iconButtonColors( @@ -307,7 +323,13 @@ fun NewWorkspaceScreen( onConfirm = { idx-> scope.launch { sheetState.hide() }.invokeOnCompletion { icon = idx } } ) - if (isDialogVisible) { SetPasskeyDialog(passkey,{ passkey = it }) { isDialogVisible = false } } + if (isDialogVisible) { + SetPasskeyDialog( + key = pendingPasskey, + onConfirmRequest = { newRaw -> pendingPasskey = newRaw }, + onDismissRequest = { isDialogVisible = false } + ) + } } @Composable @@ -350,4 +372,4 @@ fun SelectedSpacesOrderEditor( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceComponents.kt b/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceComponents.kt index 739ea25..91727c0 100644 --- a/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceComponents.kt +++ b/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceComponents.kt @@ -72,8 +72,19 @@ import com.flux.ui.screens.settings.shapeManager // ------------- Dialog ------------- @Composable -fun SetPasskeyDialog(key: String?=null, onConfirmRequest: (String) -> Unit, onDismissRequest: () -> Unit) { - var passKey by remember { mutableStateOf(key?: "") } +fun SetPasskeyDialog( + key: String? = null, + onConfirmRequest: (String) -> Unit, + onDismissRequest: () -> Unit +) { + var passKey by remember { mutableStateOf(key ?: "") } + val isValid = passKey.isNotBlank() + + fun confirmIfValid() { + if (!isValid) return + onConfirmRequest(passKey) + onDismissRequest() + } Dialog(onDismissRequest) { Card(shape = RoundedCornerShape(16.dp)) { @@ -93,24 +104,20 @@ fun SetPasskeyDialog(key: String?=null, onConfirmRequest: (String) -> Unit, onDi singleLine = true, onValueChange = { passKey = it }, modifier = Modifier.fillMaxWidth(), + isError = passKey.isNotEmpty() && !isValid, keyboardOptions = KeyboardOptions.Default.copy( imeAction = ImeAction.Done ), keyboardActions = KeyboardActions( - onDone = { - onConfirmRequest(passKey) - onDismissRequest() - } + onDone = { confirmIfValid() } ) ) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { TextButton(onDismissRequest) { Text(stringResource(R.string.Dismiss)) } TextButton( - onClick = { - onConfirmRequest(passKey) - onDismissRequest() - }, + onClick = { confirmIfValid() }, + enabled = isValid, colors = ButtonDefaults.buttonColors() ) { Text(stringResource(R.string.Confirm)) } } diff --git a/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceDetails.kt b/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceDetails.kt index 98209af..6d0fe2c 100644 --- a/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceDetails.kt +++ b/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceDetails.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavController import com.flux.data.model.WorkspaceModel import com.flux.data.model.getSpacesList +import com.flux.data.model.lockWith +import com.flux.data.model.removePasskey import com.flux.other.ensureStorageRoot import com.flux.ui.common.DeleteAlert import com.flux.ui.events.HabitEvents @@ -87,8 +89,8 @@ fun WorkspaceDetails( onRemoveCover = { viewModels.workspaceViewModel.onEvent(WorkspaceEvents.UpsertSpace(workspace.copy(cover = ""))) }, onDeleteWorkspace = { isDeleteDialogVisible = true }, onToggleLock = { - if(workspace.passKey!=null) { - viewModels.workspaceViewModel.onEvent(WorkspaceEvents.UpsertSpace(workspace.copy(passKey = null))) + if(workspace.isLocked) { + viewModels.workspaceViewModel.onEvent(WorkspaceEvents.UpsertSpace(workspace.removePasskey())) } else { isPasskeyDialogVisible = true } }, @@ -99,7 +101,7 @@ fun WorkspaceDetails( if (isPasskeyDialogVisible) { SetPasskeyDialog( onConfirmRequest = { - viewModels.workspaceViewModel.onEvent(WorkspaceEvents.UpsertSpace(workspace.copy(passKey = it))) + viewModels.workspaceViewModel.onEvent(WorkspaceEvents.UpsertSpace(workspace.lockWith(it))) }, onDismissRequest = { isPasskeyDialogVisible = false } ) diff --git a/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceHomeScreen.kt b/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceHomeScreen.kt index e6482a1..e3f06b3 100644 --- a/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceHomeScreen.kt +++ b/app/src/main/java/com/flux/ui/screens/workspaces/WorkspaceHomeScreen.kt @@ -38,6 +38,7 @@ import com.flux.data.model.WorkspaceModel import com.flux.navigation.NavRoutes import com.flux.ui.events.WorkspaceEvents import com.flux.R +import com.flux.data.model.verifyPasskey import com.flux.ui.common.BottomBar import com.flux.ui.common.SelectedToolBarRow import com.flux.ui.state.States @@ -61,7 +62,7 @@ fun WorkspaceHomeScreen( lockedWorkspace?.let { SetPasskeyDialog(onConfirmRequest = { passkey -> - if (it.passKey == passkey) { + if (it.verifyPasskey(passkey)) { navController.navigate(NavRoutes.WorkspaceHome.withArgs(it.workspaceId)) } else { Toast.makeText(context, wrongPassKeyLabel, Toast.LENGTH_SHORT).show() @@ -70,10 +71,8 @@ fun WorkspaceHomeScreen( } fun handleWorkspaceClick(space: WorkspaceModel) { - if (space.passKey!=null) { lockedWorkspace = space } - else { - navController.navigate(NavRoutes.WorkspaceHome.withArgs(space.workspaceId)) - } + if (space.isLocked) { lockedWorkspace = space } + else { navController.navigate(NavRoutes.WorkspaceHome.withArgs(space.workspaceId)) } } Scaffold( @@ -156,7 +155,7 @@ fun WorkspaceHomeScreen( gridColumns = gridColumns, iconIndex = space.icon, radius = radius, - isLocked = space.passKey != null, + isLocked = space.isLocked, cover = space.cover, title = space.title, description = space.description, @@ -185,7 +184,7 @@ fun WorkspaceHomeScreen( gridColumns = gridColumns, iconIndex = space.icon, radius = radius, - isLocked = space.passKey != null, + isLocked = space.isLocked, cover = space.cover, title = space.title, description = space.description,