mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-19 13:35:57 +00:00
feat: require PIN to delete protected profiles
This commit is contained in:
parent
599e82e7ff
commit
b7d588f9c8
5 changed files with 41 additions and 12 deletions
|
|
@ -227,7 +227,7 @@ fun FluxaApp(
|
|||
onRemoveAvatarClick: () -> Unit = {},
|
||||
onPickPackAvatarClick: (String) -> Unit = {},
|
||||
onProfileSave: (ProfileEditUiModel) -> Unit = {},
|
||||
onProfileDelete: (() -> Unit)? = null,
|
||||
onProfileDelete: (suspend (String?) -> Boolean)? = null,
|
||||
onProfileEditCancel: () -> Unit = {},
|
||||
onPickBackgroundClick: () -> Unit = {},
|
||||
playerState: PlayerRenderState? = null,
|
||||
|
|
|
|||
|
|
@ -571,12 +571,12 @@ fun FluxaAppHost(
|
|||
}
|
||||
},
|
||||
onProfileDelete = (appState.uiState.editingProfile as? ProfileEditTarget.Existing)?.let { existing ->
|
||||
{
|
||||
scope.launch {
|
||||
profileStore?.deleteProfile(existing.id)
|
||||
{ pin ->
|
||||
val deleted = profileStore?.deleteProfile(existing.id, pin) == true
|
||||
if (deleted) {
|
||||
appState.beginProfileEdit(null)
|
||||
}
|
||||
Unit
|
||||
deleted
|
||||
}
|
||||
},
|
||||
onProfileEditCancel = { appState.beginProfileEdit(null) },
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ interface ProfileDataSource {
|
|||
suspend fun attemptPin(profileId: String, pin: String)
|
||||
suspend fun confirmBiometricUnlock(profileId: String)
|
||||
suspend fun cancelPinUnlock()
|
||||
suspend fun deleteProfile(id: String)
|
||||
suspend fun deleteProfile(id: String, pin: String? = null): Boolean
|
||||
suspend fun saveProfile(edit: ProfileEditUiModel): String
|
||||
suspend fun setPickerBackground(url: String?)
|
||||
suspend fun addAvatarPack(repositoryUrl: String)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -49,6 +50,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import com.fluxa.app.common.AppStrings
|
||||
import com.fluxa.app.shared.feature.settings.SettingsGroupCard
|
||||
import com.fluxa.app.shared.feature.settings.SettingsSectionHeader
|
||||
|
|
@ -65,7 +67,7 @@ fun ProfileEditScreen(
|
|||
onPickAvatarClick: () -> Unit,
|
||||
onRemoveAvatarClick: () -> Unit,
|
||||
onSave: (ProfileEditUiModel) -> Unit,
|
||||
onDelete: (() -> Unit)?,
|
||||
onDelete: (suspend (String?) -> Boolean)?,
|
||||
onCancel: () -> Unit,
|
||||
avatarPacks: List<ProfileAvatarPackUiModel> = emptyList(),
|
||||
onPickPackAvatarClick: (String) -> Unit = {},
|
||||
|
|
@ -77,6 +79,9 @@ fun ProfileEditScreen(
|
|||
var biometricEnabled by remember(initialProfile?.id) { mutableStateOf(initialProfile?.biometricEnabled == true) }
|
||||
var showAvatarSheet by remember { mutableStateOf(false) }
|
||||
var confirmDelete by remember(initialProfile?.id) { mutableStateOf(false) }
|
||||
var deletePin by remember(initialProfile?.id) { mutableStateOf("") }
|
||||
var deletePinError by remember(initialProfile?.id) { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val pinValid = pin.isEmpty() || pin.length == 4
|
||||
val willHavePin = !removePin && (pin.length == 4 || initialProfile?.hasPin == true)
|
||||
|
|
@ -228,11 +233,30 @@ fun ProfileEditScreen(
|
|||
AlertDialog(
|
||||
onDismissRequest = { confirmDelete = false },
|
||||
title = { Text(AppStrings.t(language, "profiles.delete_confirm_title")) },
|
||||
text = { Text(AppStrings.t(language, "profiles.delete_confirm_message")) },
|
||||
text = {
|
||||
Column {
|
||||
Text(AppStrings.t(language, "profiles.delete_confirm_message"))
|
||||
if (initialProfile?.hasPin == true) {
|
||||
OutlinedTextField(
|
||||
value = deletePin,
|
||||
onValueChange = {
|
||||
deletePin = it.filter(Char::isDigit).take(4)
|
||||
deletePinError = false
|
||||
},
|
||||
label = { Text(AppStrings.t(language, "profiles.pin_prompt")) },
|
||||
isError = deletePinError,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
confirmDelete = false
|
||||
onDelete()
|
||||
scope.launch {
|
||||
val deleted = onDelete(if (initialProfile?.hasPin == true) deletePin else null)
|
||||
if (deleted) confirmDelete = false else deletePinError = true
|
||||
}
|
||||
}) { Text(AppStrings.t(language, "profiles.delete"), color = FluxaColors.errorRed) }
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ProfileStore(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun deleteProfile(id: String) = dataSource.deleteProfile(id)
|
||||
suspend fun deleteProfile(id: String, pin: String? = null): Boolean = dataSource.deleteProfile(id, pin)
|
||||
|
||||
suspend fun saveProfile(edit: ProfileEditUiModel): String = dataSource.saveProfile(edit)
|
||||
}
|
||||
|
|
@ -116,7 +116,12 @@ class SharedProfileDataSource(
|
|||
pinError.value = false
|
||||
}
|
||||
|
||||
override suspend fun deleteProfile(id: String) = store.delete(id)
|
||||
override suspend fun deleteProfile(id: String, pin: String?): Boolean {
|
||||
val expectedPinHash = store.pinHash(id)
|
||||
if (!expectedPinHash.isNullOrBlank() && PinHasher.hash(pin.orEmpty()) != expectedPinHash) return false
|
||||
store.delete(id)
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun saveProfile(edit: ProfileEditUiModel): String {
|
||||
val pinHash = when {
|
||||
|
|
|
|||
Loading…
Reference in a new issue