Persist biometric access
This commit is contained in:
@@ -18,8 +18,8 @@ android {
|
||||
applicationId "net.linkedcorp.linkedsign"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 10103
|
||||
versionName "1.1.3"
|
||||
versionCode 10200
|
||||
versionName "1.2.0"
|
||||
|
||||
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
||||
}
|
||||
|
||||
@@ -29,12 +29,21 @@ interface LinkedSignApiService {
|
||||
@POST("auth/refresh")
|
||||
suspend fun refresh(@Body body: RefreshRequest): ApiEnvelope<RefreshData>
|
||||
|
||||
@POST("auth/biometric-login")
|
||||
suspend fun biometricLogin(@Body body: BiometricTokenRequest): ApiEnvelope<BiometricLoginData>
|
||||
|
||||
@POST("auth/logout")
|
||||
suspend fun logout(@Body body: RefreshRequest): ApiEnvelope<SimpleMessageData>
|
||||
|
||||
@GET("auth/me")
|
||||
suspend fun me(@Header("Authorization") authorization: String): ApiEnvelope<MeData>
|
||||
|
||||
@POST("auth/biometric-token")
|
||||
suspend fun issueBiometricToken(@Header("Authorization") authorization: String): ApiEnvelope<BiometricTokenData>
|
||||
|
||||
@DELETE("auth/biometric-token")
|
||||
suspend fun revokeBiometricToken(@Header("Authorization") authorization: String): ApiEnvelope<SimpleMessageData>
|
||||
|
||||
@PATCH("auth/change-password")
|
||||
suspend fun changePassword(
|
||||
@Header("Authorization") authorization: String,
|
||||
|
||||
@@ -77,7 +77,6 @@ class LinkedSignRepository(private val context: Context) {
|
||||
)
|
||||
sessionStore.save(session)
|
||||
localCache.saveProfile(response.user)
|
||||
sessionStore.updateBiometricSession(response.user.email, response.refreshToken)
|
||||
session
|
||||
}
|
||||
}
|
||||
@@ -110,41 +109,52 @@ class LinkedSignRepository(private val context: Context) {
|
||||
}
|
||||
|
||||
suspend fun biometricLogin(): SessionSnapshot {
|
||||
val biometric = sessionStore.loadBiometricRefreshToken()
|
||||
val biometric = sessionStore.loadBiometricToken()
|
||||
?: throw AppException("No hay sesión biométrica guardada.")
|
||||
return execute {
|
||||
val refresh = unwrap(service.refresh(RefreshRequest(biometric.second)))
|
||||
val user = unwrap(service.me("Bearer ${refresh.accessToken}")).user
|
||||
val session = SessionSnapshot(
|
||||
accessToken = refresh.accessToken,
|
||||
refreshToken = refresh.refreshToken,
|
||||
user = user
|
||||
)
|
||||
sessionStore.save(session)
|
||||
localCache.saveProfile(user)
|
||||
sessionStore.updateBiometricSession(user.email, refresh.refreshToken)
|
||||
session
|
||||
return try {
|
||||
execute {
|
||||
val response = unwrap(service.biometricLogin(BiometricTokenRequest(biometric.second)))
|
||||
val session = SessionSnapshot(
|
||||
accessToken = response.accessToken,
|
||||
refreshToken = response.refreshToken,
|
||||
user = response.user
|
||||
)
|
||||
sessionStore.save(session)
|
||||
localCache.saveProfile(response.user)
|
||||
sessionStore.updateBiometricSession(response.user.email, response.biometricToken)
|
||||
session
|
||||
}
|
||||
} catch (error: AppException) {
|
||||
if (error.code == "BIOMETRIC_EXPIRED" || error.code == "BIOMETRIC_INVALIDATED") {
|
||||
sessionStore.declineBiometricAccess()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
fun biometricState(): BiometricAccessState = sessionStore.loadBiometricState()
|
||||
|
||||
fun enableBiometricAccess() {
|
||||
suspend fun enableBiometricAccess() {
|
||||
val session = sessionStore.load()
|
||||
val user = session.user ?: throw AppException("Primero debes iniciar sesión.")
|
||||
val refreshToken = session.refreshToken ?: throw AppException("No hay sesión para activar biometría.")
|
||||
sessionStore.saveBiometricAccess(user.email, refreshToken)
|
||||
val accessToken = session.accessToken ?: throw AppException("No hay sesión para activar biometría.")
|
||||
val response = execute {
|
||||
unwrap(service.issueBiometricToken("Bearer $accessToken"))
|
||||
}
|
||||
sessionStore.saveBiometricAccess(user.email, response.biometricToken)
|
||||
}
|
||||
|
||||
fun declineBiometricAccess() {
|
||||
sessionStore.declineBiometricAccess()
|
||||
}
|
||||
|
||||
fun syncBiometricSession() {
|
||||
suspend fun declineBiometricAccess() {
|
||||
val session = sessionStore.load()
|
||||
val user = session.user ?: return
|
||||
val refreshToken = session.refreshToken ?: return
|
||||
sessionStore.updateBiometricSession(user.email, refreshToken)
|
||||
val accessToken = session.accessToken
|
||||
if (!accessToken.isNullOrBlank()) {
|
||||
runCatching {
|
||||
execute {
|
||||
unwrap(service.revokeBiometricToken("Bearer $accessToken"))
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionStore.declineBiometricAccess()
|
||||
}
|
||||
|
||||
suspend fun requestPasswordChange(currentPassword: String, newPassword: String): SimpleMessageData =
|
||||
@@ -243,7 +253,6 @@ class LinkedSignRepository(private val context: Context) {
|
||||
refreshToken = refresh.refreshToken
|
||||
)
|
||||
sessionStore.save(updated)
|
||||
sessionStore.updateBiometricSession(updated.user?.email ?: session.user?.email.orEmpty(), refresh.refreshToken)
|
||||
return updated
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,21 @@ data class LoginData(
|
||||
val user: SessionUser
|
||||
)
|
||||
|
||||
data class BiometricTokenRequest(
|
||||
val biometricToken: String
|
||||
)
|
||||
|
||||
data class BiometricTokenData(
|
||||
val biometricToken: String
|
||||
)
|
||||
|
||||
data class BiometricLoginData(
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
val biometricToken: String,
|
||||
val user: SessionUser
|
||||
)
|
||||
|
||||
data class RegisterData(
|
||||
val message: String?,
|
||||
val userId: Int?
|
||||
|
||||
@@ -75,22 +75,22 @@ class SessionStore(context: Context) {
|
||||
val enabled = preferences.getBoolean(KEY_BIO_ENABLED, false)
|
||||
val configured = preferences.getBoolean(KEY_BIO_CONFIGURED, false)
|
||||
val email = preferences.getString(KEY_BIO_EMAIL, null)
|
||||
val refreshToken = preferences.getString(KEY_BIO_REFRESH, null)
|
||||
val biometricToken = preferences.getString(KEY_BIO_TOKEN, null)
|
||||
|
||||
return BiometricAccessState(
|
||||
enabled = enabled,
|
||||
configured = configured,
|
||||
email = email,
|
||||
hasCredentials = !email.isNullOrBlank() && !refreshToken.isNullOrBlank()
|
||||
hasCredentials = !email.isNullOrBlank() && !biometricToken.isNullOrBlank()
|
||||
)
|
||||
}
|
||||
|
||||
fun saveBiometricAccess(email: String, refreshToken: String) {
|
||||
fun saveBiometricAccess(email: String, biometricToken: String) {
|
||||
preferences.edit()
|
||||
.putBoolean(KEY_BIO_ENABLED, true)
|
||||
.putBoolean(KEY_BIO_CONFIGURED, true)
|
||||
.putString(KEY_BIO_EMAIL, email)
|
||||
.putString(KEY_BIO_REFRESH, refreshToken)
|
||||
.putString(KEY_BIO_TOKEN, biometricToken)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -99,28 +99,28 @@ class SessionStore(context: Context) {
|
||||
.putBoolean(KEY_BIO_ENABLED, false)
|
||||
.putBoolean(KEY_BIO_CONFIGURED, true)
|
||||
.remove(KEY_BIO_EMAIL)
|
||||
.remove(KEY_BIO_REFRESH)
|
||||
.remove(KEY_BIO_TOKEN)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun updateBiometricSession(email: String, refreshToken: String) {
|
||||
fun updateBiometricSession(email: String, biometricToken: String) {
|
||||
if (!preferences.getBoolean(KEY_BIO_ENABLED, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
preferences.edit()
|
||||
.putString(KEY_BIO_EMAIL, email)
|
||||
.putString(KEY_BIO_REFRESH, refreshToken)
|
||||
.putString(KEY_BIO_TOKEN, biometricToken)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun loadBiometricRefreshToken(): Pair<String, String>? {
|
||||
fun loadBiometricToken(): Pair<String, String>? {
|
||||
val email = preferences.getString(KEY_BIO_EMAIL, null)
|
||||
val refreshToken = preferences.getString(KEY_BIO_REFRESH, null)
|
||||
if (email.isNullOrBlank() || refreshToken.isNullOrBlank()) {
|
||||
val biometricToken = preferences.getString(KEY_BIO_TOKEN, null)
|
||||
if (email.isNullOrBlank() || biometricToken.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return email to refreshToken
|
||||
return email to biometricToken
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -131,7 +131,7 @@ class SessionStore(context: Context) {
|
||||
private const val KEY_BIO_ENABLED = "bio_enabled"
|
||||
private const val KEY_BIO_CONFIGURED = "bio_configured"
|
||||
private const val KEY_BIO_EMAIL = "bio_email"
|
||||
private const val KEY_BIO_REFRESH = "bio_refresh_token"
|
||||
private const val KEY_BIO_TOKEN = "bio_token"
|
||||
private const val DEFAULT_SESSION_MS = 15 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
appUnlocked = true
|
||||
} else {
|
||||
authMode = AuthMode.Login
|
||||
viewModel.declineBiometricAccess()
|
||||
appUnlocked = true
|
||||
}
|
||||
},
|
||||
@@ -455,8 +454,10 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
if (showBiometricOptIn) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.declineBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
scope.launch {
|
||||
viewModel.declineBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
}
|
||||
},
|
||||
title = { Text("Activar acceso biométrico") },
|
||||
text = {
|
||||
@@ -464,16 +465,23 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
viewModel.enableBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
scope.launch {
|
||||
runCatching { viewModel.enableBiometricAccess() }
|
||||
.onSuccess { showBiometricOptIn = false }
|
||||
.onFailure {
|
||||
snackbarHostState.showSnackbar((it as? AppException)?.message ?: "No se pudo activar la huella.")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text("Sí, usar huella")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.declineBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
scope.launch {
|
||||
viewModel.declineBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
}
|
||||
}) {
|
||||
Text("Ahora no")
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
suspend fun login(email: String, password: String) {
|
||||
val session = repository.login(email, password)
|
||||
repository.syncBiometricSession()
|
||||
val (_, cache) = repository.loadLocalSnapshot()
|
||||
uiState = buildState(session, cache, loading = false)
|
||||
}
|
||||
@@ -162,12 +161,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
suspend fun accessToken(): String? = repository.getFreshAccessToken()
|
||||
|
||||
fun enableBiometricAccess() {
|
||||
suspend fun enableBiometricAccess() {
|
||||
repository.enableBiometricAccess()
|
||||
refreshState()
|
||||
}
|
||||
|
||||
fun declineBiometricAccess() {
|
||||
suspend fun declineBiometricAccess() {
|
||||
repository.declineBiometricAccess()
|
||||
refreshState()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user