Replace runBlocking with suspend functions for PIN storage

Make loadPinFromEncryptedStorage and savePinToEncryptedStorage suspend
functions, and update all callers (SecurityScreen, ConfirmPinScreen,
BiometricAuthScreen) to use coroutine scopes instead of blocking the
calling thread.

https://claude.ai/code/session_01TeTHfGJL42J7eHCDdhsDnP
This commit is contained in:
Claude
2026-03-13 11:09:47 +00:00
parent d039cacde9
commit 75de5f3e0a
4 changed files with 56 additions and 40 deletions
@@ -157,20 +157,20 @@ object LocalPreferences {
}
}
fun loadPinFromEncryptedStorage(): String? {
suspend fun loadPinFromEncryptedStorage(): String? {
val context = Amber.instance
// Migration: move PIN from plain SharedPreferences to encrypted DataStore
val legacyPin = sharedPrefs(context).getString(SettingsKeys.PIN.key, null)
if (legacyPin != null) {
runBlocking { DataStoreAccess.savePin(context, legacyPin) }
DataStoreAccess.savePin(context, legacyPin)
sharedPrefs(context).edit { remove(SettingsKeys.PIN.key) }
}
return runBlocking { DataStoreAccess.loadPin(context) }
return DataStoreAccess.loadPin(context)
}
fun savePinToEncryptedStorage(pin: String?) {
suspend fun savePinToEncryptedStorage(pin: String?) {
val context = Amber.instance
runBlocking { DataStoreAccess.savePin(context, pin) }
DataStoreAccess.savePin(context, pin)
}
suspend fun reloadApp() {
@@ -90,8 +90,12 @@ fun SecurityScreen(
.padding(horizontal = 8.dp)
.clickable {
if (setupPin) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
scope.launch(Dispatchers.IO) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
scope.launch(Dispatchers.Main) {
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
}
}
} else {
navController.navigate(Route.SetupPin.route)
}
@@ -105,8 +109,12 @@ fun SecurityScreen(
checked = setupPin,
onCheckedChange = {
if (setupPin) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
scope.launch(Dispatchers.IO) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
scope.launch(Dispatchers.Main) {
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
}
}
} else {
navController.navigate(Route.SetupPin.route)
}
@@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
@@ -10,6 +11,8 @@ import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.ui.components.RandomPinInput
import com.greenart7c3.nostrsigner.ui.navigation.Route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun SetupPinScreen(
@@ -32,6 +35,7 @@ fun ConfirmPinScreen(
navController: NavController,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier,
) {
@@ -41,22 +45,21 @@ fun ConfirmPinScreen(
ToastManager.toast(context.getString(R.string.pin), context.getString(R.string.pin_does_not_match))
} else {
val usePin = Amber.instance.settings.usePin
if (usePin) {
Amber.instance.settings = Amber.instance.settings.copy(usePin = false)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(null)
navController.navigate(Route.Security.route) {
popUpTo(Route.Security.route) {
inclusive = true
}
scope.launch(Dispatchers.IO) {
if (usePin) {
Amber.instance.settings = Amber.instance.settings.copy(usePin = false)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(null)
} else {
Amber.instance.settings = Amber.instance.settings.copy(usePin = true)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(pin)
}
} else {
Amber.instance.settings = Amber.instance.settings.copy(usePin = true)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(pin)
navController.navigate(Route.Security.route) {
popUpTo(Route.Security.route) {
inclusive = true
scope.launch(Dispatchers.Main) {
navController.navigate(Route.Security.route) {
popUpTo(Route.Security.route) {
inclusive = true
}
}
}
}
@@ -15,6 +15,7 @@ import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -36,6 +37,7 @@ fun BiometricAuthScreen(
onAuth: (Boolean) -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showPinDialog by remember { mutableStateOf(false) }
val keyguardLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
@@ -117,22 +119,25 @@ fun BiometricAuthScreen(
) {
RandomPinInput(
text = stringResource(R.string.enter_pin),
onPinEntered = {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
if (it == pin) {
Amber.instance.settings = Amber.instance.settings.copy(
lastBiometricsTime = System.currentTimeMillis(),
)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
onAuth(true)
showPinDialog = false
} else {
Toast.makeText(
context,
context.getString(R.string.pin_does_not_match),
Toast.LENGTH_SHORT,
).show()
onPinEntered = { enteredPin ->
scope.launch(Dispatchers.IO) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
scope.launch(Dispatchers.Main) {
if (enteredPin == pin) {
Amber.instance.settings = Amber.instance.settings.copy(
lastBiometricsTime = System.currentTimeMillis(),
)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
onAuth(true)
showPinDialog = false
} else {
Toast.makeText(
context,
context.getString(R.string.pin_does_not_match),
Toast.LENGTH_SHORT,
).show()
}
}
}
},
)