From a2c3b32391688833146a19b0fa6378cff928f2f9 Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Fri, 30 Jan 2026 15:48:35 -0500 Subject: [PATCH 1/5] Implement conditional create in Shrine --- .../shrine/CredentialManagerUtils.kt | 7 ++- .../shrine/api/AuthApiService.kt | 35 +++------------ .../shrine/model/LoginRequest.kt | 22 +++------- .../shrine/repository/AuthRepository.kt | 43 +------------------ .../shrine/ui/AuthenticationScreen.kt | 16 ++++++- .../ui/viewmodel/AuthenticationViewModel.kt | 26 +++++++++++ 6 files changed, 60 insertions(+), 89 deletions(-) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt index d368874..7e9e816 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt @@ -171,19 +171,24 @@ class CredentialManagerUtils @Inject constructor( * * @param requestResult The result of the passkey creation request. * @param context The activity context from the Composable, to be used in Credential Manager APIs + * @param isConditional Whether the passkey creation is conditional. * @return The [CreatePublicKeyCredentialResponse] object containing the passkey, or null if an error occurred. */ @SuppressLint("PublicKeyCredential") suspend fun createPasskey( requestResult: JSONObject, context: Context, + isConditional: Boolean = false, ): GenericCredentialManagerResponse { val passkeysEligibility = PasskeysEligibility.isPasskeySupported(context) if (!passkeysEligibility.isEligible) { return GenericCredentialManagerResponse.Error(errorMessage = passkeysEligibility.reason) } - val credentialRequest = CreatePublicKeyCredentialRequest(requestResult.toString()) + val credentialRequest = CreatePublicKeyCredentialRequest( + requestJson = requestResult.toString(), + isConditional = isConditional + ) val credentialResponse: CreatePublicKeyCredentialResponse try { credentialResponse = credentialManager.createCredential( diff --git a/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt index 7de9642..d352dc6 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt @@ -18,6 +18,7 @@ package com.authentication.shrine.api import com.authentication.shrine.model.EditUsernameRequest import com.authentication.shrine.model.FederationOptionsRequest import com.authentication.shrine.model.GenericAuthResponse +import com.authentication.shrine.model.LoginUsernamePasswordRequest import com.authentication.shrine.model.PasskeysList import com.authentication.shrine.model.PasswordRequest import com.authentication.shrine.model.RegisterRequestRequestBody @@ -40,41 +41,17 @@ import retrofit2.http.Query interface AuthApiService { /** - * Sets or updates the username for the current session. + * Logs in with the username and password * - * @param username The request body containing the new username. + * @param usernamePasswordRequest The request body containing the username and password. * @return A Retrofit {@link Response} wrapping a {@link GenericAuthResponse}, * indicating the success or failure of the operation. */ - @POST("auth/username") - suspend fun setUsername( - @Body username: EditUsernameRequest, + @POST("auth/username-password") + suspend fun loginWithUsernamePassword( + @Body usernamePasswordRequest: LoginUsernamePasswordRequest, ): Response - /** - * Sets or updates the password for the authenticated user. - * - * @param cookie The session cookie for authentication. - * @param password The request body containing the new password information. - * @return A Retrofit {@link Response} wrapping a {@link GenericAuthResponse}, - * indicating the success or failure of the operation. - */ - @POST("auth/password") - suspend fun setPassword( - @Header("Cookie") cookie: String, - @Body password: PasswordRequest, - ): Response - - /** - * Initiates a WebAuthn registration ceremony by requesting registration options - * from the server. - * - * @param cookie The session cookie for authentication. - * @param requestBody The request body, potentially containing user information - * or relying party details for the registration request. - * @return A Retrofit {@link Response} wrapping a {@link RegisterRequestResponse}, - * which contains the challenge and options for the WebAuthn registration. - */ @POST("webauthn/registerRequest") suspend fun registerRequest( @Header("Cookie") cookie: String, diff --git a/Shrine/app/src/main/java/com/authentication/shrine/model/LoginRequest.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/LoginRequest.kt index e62da84..61dc48a 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/model/LoginRequest.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/model/LoginRequest.kt @@ -16,14 +16,15 @@ package com.authentication.shrine.model /** - * Represents the request body for setting or updating an existing username. - * This data class is typically used for serialization (e.g., with Gson or Kotlinx Serialization) - * when making API calls related to username management. + * Represents the request body for logging in with a username and password. * - * @property username The username to be set or updated. + * @property username The username to log in with. + * @property password The password to log in with. */ -data class EditUsernameRequest( + +data class LoginUsernamePasswordRequest( val username: String, + val password: String, ) /** @@ -38,14 +39,3 @@ data class RegisterUsernameRequest( val username: String, val displayName: String, ) - -/** - * Represents the request body for setting or updating a password. - * This data class is typically used for serialization (e.g., with Gson or Kotlinx Serialization) - * when making API calls related to password management. - * - * @property password The new password. - */ -data class PasswordRequest( - val password: String, -) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt index 81b475f..37c0244 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt @@ -36,6 +36,7 @@ import com.authentication.shrine.model.AuthResult import com.authentication.shrine.model.CredmanResponse import com.authentication.shrine.model.EditUsernameRequest import com.authentication.shrine.model.FederationOptionsRequest +import com.authentication.shrine.model.LoginUsernamePasswordRequest import com.authentication.shrine.model.PasskeysList import com.authentication.shrine.model.PasswordRequest import com.authentication.shrine.model.RegisterRequestRequestBody @@ -140,7 +141,7 @@ class AuthRepository @Inject constructor( */ suspend fun login(username: String, password: String): AuthResult { return try { - val response = authApiService.setUsername(EditUsernameRequest(username = username)) + val response = authApiService.loginWithUsernamePassword(LoginUsernamePasswordRequest(username, password)) if (response.isSuccessful) { dataStore.edit { prefs -> prefs[USERNAME] = response.body()?.username.orEmpty() @@ -149,7 +150,6 @@ class AuthRepository @Inject constructor( prefs[SESSION_ID] = it } } - setSessionWithPassword(password) AuthResult.Success(Unit) } else { if (response.code() == 401) { @@ -166,45 +166,6 @@ class AuthRepository @Inject constructor( } } - /** - * Signs in with a password. - * - * @param password The password to use. - * @return True if the sign-in was successful, false otherwise. - */ - private suspend fun setSessionWithPassword(password: String): Boolean { - val username = dataStore.read(USERNAME) - val sessionId = dataStore.read(SESSION_ID) - if (!username.isNullOrEmpty() && !sessionId.isNullOrEmpty()) { - try { - val response = authApiService.setPassword( - cookie = sessionId.createCookieHeader(), - password = PasswordRequest(password = password), - ) - if (response.isSuccessful) { - dataStore.edit { prefs -> - prefs[USERNAME] = response.body()?.username.orEmpty() - prefs[DISPLAYNAME] = response.body()?.displayName.orEmpty() - response.getSessionId()?.also { - prefs[SESSION_ID] = it - } - } - return true - } else if (response.code() == 401) { - signOut() - } - } catch (e: ApiException) { - Log.e(TAG, "Invalid login credentials", e) - - // Remove previously stored credentials and start login over again - signOut() - } - } else { - Log.e(TAG, "Please check if username and session id is present in your datastore") - } - return false - } - /** * Clears all the sign-in information. */ diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt index 927044b..343e459 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt @@ -49,6 +49,7 @@ import com.authentication.shrine.ui.common.ShrineLoader import com.authentication.shrine.ui.theme.ShrineTheme import com.authentication.shrine.ui.viewmodel.AuthenticationUiState import com.authentication.shrine.ui.viewmodel.AuthenticationViewModel +import org.json.JSONObject /** * Stateful composable function for Authentication screen. @@ -86,9 +87,20 @@ fun AuthenticationScreen( val onSignInWithPasskeyOrPasswordRequest = { viewModel.signInWithPasskeyOrPasswordRequest( - onSuccess = { flag -> + onSuccess = { isPasswordCredential -> + if (isPasswordCredential) { + viewModel.conditionalCreatePasskey( + createPasskeyOnCredMan = { createPasskeyRequestObj: JSONObject -> + credentialManagerUtils.createPasskey( + requestResult = createPasskeyRequestObj, + context = context, + isConditional = true, + ) + } + ) + } createRestoreKey() - navigateToHome(flag) + navigateToHome(!isPasswordCredential) }, getCredential = { jsonObject -> credentialManagerUtils.getPasskeyOrPasswordCredential( diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt index c09b352..e3211c7 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt @@ -302,6 +302,32 @@ class AuthenticationViewModel @Inject constructor( } } } + + /** + * Conditionally creates a passkey after a successful password login. + * + * @param createPasskeyOnCredMan A suspend function that takes a [JSONObject] and returns a + * [GenericCredentialManagerResponse]. This function is responsible for creating + * the passkey. + */ + fun conditionalCreatePasskey( + createPasskeyOnCredMan: suspend (createPasskeyRequestObj: JSONObject) -> GenericCredentialManagerResponse, + ) { + coroutineScope.launch { + when (val result = repository.registerPasskeyCreationRequest()) { + is AuthResult.Success -> { + val createPasskeyResponse = createPasskeyOnCredMan(result.data) + if (createPasskeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { + repository.registerPasskeyCreationResponse(createPasskeyResponse.createPasskeyResponse) + } + } + + is AuthResult.Failure -> { + Log.e(TAG, "Error during conditional passkey creation.") + } + } + } + } } /** From 047c321bb99b75a7d0e5cba1a5446ac5f7b534e1 Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Fri, 6 Feb 2026 16:43:46 -0500 Subject: [PATCH 2/5] Remove invalid imports, refactor some functions to be suspend for conditional create to work with create restore key, and ensure CreatePasskeyScreen isn't shown on passkey login --- .../shrine/api/AuthApiService.kt | 2 - .../shrine/repository/AuthRepository.kt | 3 - .../shrine/ui/AuthenticationScreen.kt | 15 +- .../shrine/ui/RegisterPasswordScreen.kt | 20 +- .../shrine/ui/RegisterScreen.kt | 2 +- .../shrine/ui/navigation/ShrineNavActions.kt | 4 +- .../ui/viewmodel/AuthenticationViewModel.kt | 175 +++++++++--------- .../ui/viewmodel/RegistrationViewModel.kt | 140 +++++++------- 8 files changed, 177 insertions(+), 184 deletions(-) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt index d352dc6..3f78aad 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt @@ -15,12 +15,10 @@ */ package com.authentication.shrine.api -import com.authentication.shrine.model.EditUsernameRequest import com.authentication.shrine.model.FederationOptionsRequest import com.authentication.shrine.model.GenericAuthResponse import com.authentication.shrine.model.LoginUsernamePasswordRequest import com.authentication.shrine.model.PasskeysList -import com.authentication.shrine.model.PasswordRequest import com.authentication.shrine.model.RegisterRequestRequestBody import com.authentication.shrine.model.RegisterRequestResponse import com.authentication.shrine.model.RegisterResponseRequestBody diff --git a/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt index 37c0244..b3c181a 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt @@ -29,16 +29,13 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import com.authentication.shrine.api.ApiException import com.authentication.shrine.api.AuthApiService import com.authentication.shrine.model.AuthError import com.authentication.shrine.model.AuthResult import com.authentication.shrine.model.CredmanResponse -import com.authentication.shrine.model.EditUsernameRequest import com.authentication.shrine.model.FederationOptionsRequest import com.authentication.shrine.model.LoginUsernamePasswordRequest import com.authentication.shrine.model.PasskeysList -import com.authentication.shrine.model.PasswordRequest import com.authentication.shrine.model.RegisterRequestRequestBody import com.authentication.shrine.model.RegisterResponseRequestBody import com.authentication.shrine.model.RegisterUsernameRequest diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt index 343e459..9c899ff 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt @@ -74,7 +74,7 @@ fun AuthenticationScreen( // Passing in the lambda / context to the VM val context = LocalContext.current - val createRestoreKey = { + val createRestoreKey: suspend () -> Unit = { viewModel.createRestoreKey( createRestoreKeyOnCredMan = { createRestoreCredObject -> credentialManagerUtils.createRestoreKey( @@ -88,8 +88,9 @@ fun AuthenticationScreen( val onSignInWithPasskeyOrPasswordRequest = { viewModel.signInWithPasskeyOrPasswordRequest( onSuccess = { isPasswordCredential -> + var skipPasskeyPrompt = !isPasswordCredential if (isPasswordCredential) { - viewModel.conditionalCreatePasskey( + val conditionalSuccess = viewModel.conditionalCreatePasskey( createPasskeyOnCredMan = { createPasskeyRequestObj: JSONObject -> credentialManagerUtils.createPasskey( requestResult = createPasskeyRequestObj, @@ -98,9 +99,12 @@ fun AuthenticationScreen( ) } ) + createRestoreKey() + if (conditionalSuccess) { + skipPasskeyPrompt = true + } } - createRestoreKey() - navigateToHome(!isPasswordCredential) + navigateToHome(skipPasskeyPrompt) }, getCredential = { jsonObject -> credentialManagerUtils.getPasskeyOrPasswordCredential( @@ -114,9 +118,8 @@ fun AuthenticationScreen( val onSignInWithSignInWithGoogleRequest = { viewModel.signInWithGoogleRequest( onSuccess = { - createRestoreKey() // Don't suggest passkeys if user signs in with Google. - navigateToHome(false) + navigateToHome(true) }, getCredential = { credentialManagerUtils.getSignInWithGoogleCredential( diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterPasswordScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterPasswordScreen.kt index bff4d27..197729e 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterPasswordScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterPasswordScreen.kt @@ -85,23 +85,19 @@ fun RegisterPasswordScreen( var passwordVisible by remember { mutableStateOf(false) } val context = LocalContext.current - val createRestoreKey = { - viewModel.createRestoreKey( - createRestoreKeyOnCredMan = { createRestoreCredObject -> - credentialManagerUtils.createRestoreKey( - context = context, - requestResult = createRestoreCredObject, - ) - }, - ) - } - val onRegister = { emailAddress: String, registrationPassword: String -> viewModel.onPasswordRegister( username = emailAddress, password = registrationPassword, onSuccess = { flag -> - createRestoreKey() + viewModel.createRestoreKey( + createRestoreKeyOnCredMan = { createRestoreCredObject -> + credentialManagerUtils.createRestoreKey( + context = context, + requestResult = createRestoreCredObject, + ) + }, + ) navigateToHome(flag) }, createPassword = { username: String, password: String -> diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterScreen.kt index 5068b58..ed6e8b8 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/RegisterScreen.kt @@ -89,7 +89,7 @@ fun RegisterScreen( var email by remember { mutableStateOf("") } val context = LocalContext.current - val createRestoreKey = { + val createRestoreKey: suspend () -> Unit = { viewModel.createRestoreKey( createRestoreKeyOnCredMan = { createRestoreCredObject -> credentialManagerUtils.createRestoreKey( diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavActions.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavActions.kt index 28a5c2c..b5d7774 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavActions.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavActions.kt @@ -51,12 +51,12 @@ class ShrineNavActions(navController: NavHostController) { // Takes user to Home flow. val navigateToHome: (isSignInThroughPasskeys: Boolean) -> Unit = { if (it) { - navController.navigate(ShrineAppDestinations.CreatePasskeyRoute.name) { + navController.navigate(ShrineAppDestinations.MainMenuRoute.name) { popUpTo(ShrineAppDestinations.NavHostRoute.name) launchSingleTop = true } } else { - navController.navigate(ShrineAppDestinations.MainMenuRoute.name) { + navController.navigate(ShrineAppDestinations.CreatePasskeyRoute.name) { popUpTo(ShrineAppDestinations.NavHostRoute.name) launchSingleTop = true } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt index e3211c7..63e2f21 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt @@ -28,7 +28,6 @@ import com.authentication.shrine.model.AuthResult import com.authentication.shrine.repository.AuthRepository import com.authentication.shrine.repository.AuthRepository.Companion.TAG import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -51,7 +50,6 @@ import javax.inject.Inject @HiltViewModel class AuthenticationViewModel @Inject constructor( private val repository: AuthRepository, - private val coroutineScope: CoroutineScope, ) : ViewModel() { /** @@ -64,10 +62,10 @@ class AuthenticationViewModel @Inject constructor( * Requests a sign-in challenge from the server. * * @param onSuccess Lambda that handles actions on successful passkey sign-in - * @param getPasskey Lambda that calls CredManUtil's getPasskey method with Activity reference + * @param getCredential Lambda that calls CredManUtil's getPasskey method with Activity reference */ fun signInWithPasskeyOrPasswordRequest( - onSuccess: (Boolean) -> Unit, + onSuccess: suspend (Boolean) -> Unit, getCredential: suspend (JSONObject) -> GenericCredentialManagerResponse, ) { _uiState.update { AuthenticationUiState(isLoading = true) } @@ -123,34 +121,32 @@ class AuthenticationViewModel @Inject constructor( * @param response The response from the server * @param onSuccess Lambda that handles actions on successful passkey sign-in */ - private fun signInWithPasskeyOrPasswordResponse( + private suspend fun signInWithPasskeyOrPasswordResponse( response: GetCredentialResponse, - onSuccess: (navigateToHome: Boolean) -> Unit, + onSuccess: suspend (navigateToHome: Boolean) -> Unit, ) { - viewModelScope.launch { - when (repository.signInWithPasskeyOrPasswordResponse(response)) { - is AuthResult.Success -> { - val isPasswordCredential = response.credential is PasswordCredential - repository.setSignedInState(!isPasswordCredential) - _uiState.update { - it.copy( - isSignInWithPasskeysSuccess = true, - isLoading = false - ) - } - onSuccess(isPasswordCredential) + when (repository.signInWithPasskeyOrPasswordResponse(response)) { + is AuthResult.Success -> { + val isPasswordCredential = response.credential is PasswordCredential + repository.setSignedInState(!isPasswordCredential) + onSuccess(isPasswordCredential) + _uiState.update { + it.copy( + isSignInWithPasskeysSuccess = true, + isLoading = false + ) } + } - is AuthResult.Failure -> { - repository.setSignedInState(false) - repository.clearSessionIdFromDataStore() - _uiState.update { - it.copy( - passkeyResponseMessageResourceId = R.string.error_invalid_credentials, - isSignInWithPasskeysSuccess = false, - isLoading = false, - ) - } + is AuthResult.Failure -> { + repository.setSignedInState(false) + repository.clearSessionIdFromDataStore() + _uiState.update { + it.copy( + passkeyResponseMessageResourceId = R.string.error_invalid_credentials, + isSignInWithPasskeysSuccess = false, + isLoading = false, + ) } } } @@ -162,7 +158,7 @@ class AuthenticationViewModel @Inject constructor( * @param getCredential Lambda that retrieves the credential from the Credential Manager */ fun signInWithGoogleRequest( - onSuccess: (Boolean) -> Unit, + onSuccess: suspend (Boolean) -> Unit, getCredential: suspend () -> GenericCredentialManagerResponse, ) { _uiState.update { AuthenticationUiState(isLoading = true) } @@ -193,40 +189,38 @@ class AuthenticationViewModel @Inject constructor( * @response Credentials received from Credential Manager * @onSuccess Lambda that handles actions on successful Google sign in */ - fun logInWithFederatedToken( + suspend fun logInWithFederatedToken( response: GetCredentialResponse, - onSuccess: (navigateToHome: Boolean) -> Unit, + onSuccess: suspend (navigateToHome: Boolean) -> Unit, ) { - viewModelScope.launch { - // Get sessionId from the server first. - val sessionId = repository.getFederationOptions() - if (sessionId == null) { - _uiState.update { - it.copy( - isLoading = false, - logInWithFederatedTokenFailure = true, - ) - } - } else { - // Log in to server with retrieved session ID and CredMan credentials. - when (repository.signInWithFederatedTokenResponse(sessionId, response)) { - is AuthResult.Success -> { - repository.setSignedInState(flag = false) - _uiState.update { - it.copy( - isLoading = false, - ) - } - onSuccess(true) + // Get sessionId from the server first. + val sessionId = repository.getFederationOptions() + if (sessionId == null) { + _uiState.update { + it.copy( + isLoading = false, + logInWithFederatedTokenFailure = true, + ) + } + } else { + // Log in to server with retrieved session ID and CredMan credentials. + when (repository.signInWithFederatedTokenResponse(sessionId, response)) { + is AuthResult.Success -> { + repository.setSignedInState(flag = false) + onSuccess(true) + _uiState.update { + it.copy( + isLoading = false, + ) } + } - is AuthResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - logInWithFederatedTokenFailure = true, - ) - } + is AuthResult.Failure -> { + _uiState.update { + it.copy( + isLoading = false, + logInWithFederatedTokenFailure = true, + ) } } } @@ -246,7 +240,7 @@ class AuthenticationViewModel @Inject constructor( */ fun checkForStoredRestoreKey( getRestoreKey: suspend (JSONObject) -> GenericCredentialManagerResponse, - onSuccess: (Boolean) -> Unit, + onSuccess: suspend (Boolean) -> Unit, ) { viewModelScope.launch { if (!repository.isSignedInThroughPasskeys() && !repository.isSignedInThroughPassword()) { @@ -281,26 +275,13 @@ class AuthenticationViewModel @Inject constructor( * [GenericCredentialManagerResponse]. This function is responsible for creating * the restore key. * + * @return Boolean indicating success * @see GenericCredentialManagerResponse */ - fun createRestoreKey( + suspend fun createRestoreKey( createRestoreKeyOnCredMan: suspend (createRestoreCredRequestObj: JSONObject) -> GenericCredentialManagerResponse, - ) { - coroutineScope.launch { - when (val result = repository.registerPasskeyCreationRequest()) { - is AuthResult.Success -> { - val createRestoreKeyResponse = createRestoreKeyOnCredMan(result.data) - if (createRestoreKeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { - repository.registerPasskeyCreationResponse(createRestoreKeyResponse.createPasskeyResponse) - } - } - - is AuthResult.Failure -> { - Log.e(TAG, "Error creating restore key.") - // Don't block user sign in if this fails. - } - } - } + ): Boolean { + return registerPasskey(createRestoreKeyOnCredMan, "Error creating restore key.") } /** @@ -309,23 +290,39 @@ class AuthenticationViewModel @Inject constructor( * @param createPasskeyOnCredMan A suspend function that takes a [JSONObject] and returns a * [GenericCredentialManagerResponse]. This function is responsible for creating * the passkey. + * + * @return Boolean indicating success */ - fun conditionalCreatePasskey( + suspend fun conditionalCreatePasskey( createPasskeyOnCredMan: suspend (createPasskeyRequestObj: JSONObject) -> GenericCredentialManagerResponse, - ) { - coroutineScope.launch { - when (val result = repository.registerPasskeyCreationRequest()) { - is AuthResult.Success -> { - val createPasskeyResponse = createPasskeyOnCredMan(result.data) - if (createPasskeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { - repository.registerPasskeyCreationResponse(createPasskeyResponse.createPasskeyResponse) - } - } + ): Boolean { + val success = registerPasskey(createPasskeyOnCredMan, "Error during conditional passkey creation.") + if (success) { + repository.setSignedInState(true) + } + return success + } - is AuthResult.Failure -> { - Log.e(TAG, "Error during conditional passkey creation.") + /** + * Internal helper to register a passkey (normal or restore key). + */ + private suspend fun registerPasskey( + createPasskeyOnCredMan: suspend (JSONObject) -> GenericCredentialManagerResponse, + errorMessage: String + ): Boolean { + return when (val result = repository.registerPasskeyCreationRequest()) { + is AuthResult.Success -> { + val createPasskeyResponse = createPasskeyOnCredMan(result.data) + if (createPasskeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { + repository.registerPasskeyCreationResponse(createPasskeyResponse.createPasskeyResponse) is AuthResult.Success + } else { + false } } + is AuthResult.Failure -> { + Log.e(TAG, errorMessage) + false + } } } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt index 4c2dcda..562de65 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt @@ -7,7 +7,9 @@ * * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law lifeboatress or implied. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @@ -59,7 +61,7 @@ class RegistrationViewModel @Inject constructor( fun onPasskeyRegister( username: String, displayName: String, - onSuccess: (navigateToHome: Boolean) -> Unit, + onSuccess: suspend (navigateToHome: Boolean) -> Unit, createPasskeyCallback: suspend (JSONObject) -> GenericCredentialManagerResponse, ) { _uiState.update { RegisterUiState(isLoading = true) } @@ -114,72 +116,70 @@ class RegistrationViewModel @Inject constructor( * @param createPasskey Reference to [CredentialManagerUtils.createPasskey] * The boolean parameter indicates whether the user should be navigated to the home screen. */ - private fun createPasskey( - onSuccess: (navigateToHome: Boolean) -> Unit, + private suspend fun createPasskey( + onSuccess: suspend (navigateToHome: Boolean) -> Unit, createPasskey: suspend (JSONObject) -> GenericCredentialManagerResponse, ) { _uiState.update { it.copy(isLoading = true) } - viewModelScope.launch { - when (val result = repository.registerPasskeyCreationRequest()) { - is AuthResult.Success -> { - val createPasskeyResponse = createPasskey(result.data) - if (createPasskeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { - when (repository.registerPasskeyCreationResponse(createPasskeyResponse.createPasskeyResponse)) { - is AuthResult.Success -> { - _uiState.update { - it.copy( - isSuccess = true, - isLoading = false, - ) - } - onSuccess(false) - } - - is AuthResult.Failure -> { - _uiState.update { - it.copy( - isSuccess = false, - isLoading = false, - messageResourceId = R.string.some_error_occurred_please_check_logs - ) - } + when (val result = repository.registerPasskeyCreationRequest()) { + is AuthResult.Success -> { + val createPasskeyResponse = createPasskey(result.data) + if (createPasskeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { + when (repository.registerPasskeyCreationResponse(createPasskeyResponse.createPasskeyResponse)) { + is AuthResult.Success -> { + onSuccess(true) + _uiState.update { + it.copy( + isSuccess = true, + isLoading = false, + ) } } - } else if (createPasskeyResponse is GenericCredentialManagerResponse.Error) { - _uiState.update { - it.copy( - messageResourceId = R.string.some_error_occurred_please_check_logs, - errorMessage = createPasskeyResponse.errorMessage, - isLoading = false - ) - } - repository.setSignedInState(false) - } - } - is AuthResult.Failure -> { - var errorMessage: String? = null - val messageResId = when (val error = result.error) { - is AuthError.NetworkError -> R.string.error_network - is AuthError.ServerError -> { - errorMessage = error.message - R.string.error_server - } - is AuthError.Unknown -> { - errorMessage = error.message - R.string.error_unknown + is AuthResult.Failure -> { + _uiState.update { + it.copy( + isSuccess = false, + isLoading = false, + messageResourceId = R.string.some_error_occurred_please_check_logs + ) + } } - else -> R.string.error_unknown } + } else if (createPasskeyResponse is GenericCredentialManagerResponse.Error) { _uiState.update { it.copy( - messageResourceId = messageResId, - isLoading = false, - errorMessage = errorMessage + messageResourceId = R.string.some_error_occurred_please_check_logs, + errorMessage = createPasskeyResponse.errorMessage, + isLoading = false ) } - onSuccess(false) + repository.setSignedInState(false) + } + } + + is AuthResult.Failure -> { + var errorMessage: String? = null + val messageResId = when (val error = result.error) { + is AuthError.NetworkError -> R.string.error_network + is AuthError.ServerError -> { + errorMessage = error.message + R.string.error_server + } + is AuthError.Unknown -> { + errorMessage = error.message + R.string.error_unknown + } + else -> R.string.error_unknown + } + onSuccess(false) + _uiState.update { + it.copy( + messageResourceId = messageResId, + isLoading = false, + errorMessage = errorMessage + ) } } } @@ -197,7 +197,7 @@ class RegistrationViewModel @Inject constructor( fun onPasswordRegister( username: String, password: String, - onSuccess: (navigateToHome: Boolean) -> Unit, + onSuccess: suspend (navigateToHome: Boolean) -> Unit, createPassword: suspend (String, String) -> Unit, ) { _uiState.update { it.copy(isLoading = true) } @@ -207,13 +207,13 @@ class RegistrationViewModel @Inject constructor( when (val result = repository.login(username, password)) { is AuthResult.Success -> { createPassword(username, password) + onSuccess(false) _uiState.update { it.copy( isSuccess = true, isLoading = false ) } - onSuccess(true) } is AuthResult.Failure -> { @@ -259,23 +259,25 @@ class RegistrationViewModel @Inject constructor( * [GenericCredentialManagerResponse]. This function is responsible for creating * the restore key. * + * @return Boolean indicating success * @see GenericCredentialManagerResponse */ - fun createRestoreKey( + suspend fun createRestoreKey( createRestoreKeyOnCredMan: suspend (createRestoreCredRequestObj: JSONObject) -> GenericCredentialManagerResponse, - ) { - viewModelScope.launch { - when (val result = repository.registerPasskeyCreationRequest()) { - is AuthResult.Success -> { - val createRestoreKeyResponse = createRestoreKeyOnCredMan(result.data) - if (createRestoreKeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { - repository.registerPasskeyCreationResponse(createRestoreKeyResponse.createPasskeyResponse) - } + ): Boolean { + return when (val result = repository.registerPasskeyCreationRequest()) { + is AuthResult.Success -> { + val createRestoreKeyResponse = createRestoreKeyOnCredMan(result.data) + if (createRestoreKeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { + repository.registerPasskeyCreationResponse(createRestoreKeyResponse.createPasskeyResponse) is AuthResult.Success + } else { + false } + } - is AuthResult.Failure -> { - // Don't block user registration if this fails. - } + is AuthResult.Failure -> { + // Don't block user registration if this fails. + false } } } From be344a3bed7b402b3348dec98e857e02e29b8b3f Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Thu, 12 Feb 2026 16:27:53 -0500 Subject: [PATCH 3/5] Minor changes with string value and function renaming --- .../ui/viewmodel/AuthenticationViewModel.kt | 16 ++++++++-------- Shrine/app/src/main/res/values/strings.xml | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt index 63e2f21..527d8fa 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt @@ -271,7 +271,7 @@ class AuthenticationViewModel @Inject constructor( /** * Creates a restore key by registering a new passkey. * - * @param createRestoreKeyOnCredMan A suspend function that takes a [JSONObject] and returns a + * @param createRestoreKey A suspend function that takes a [JSONObject] and returns a * [GenericCredentialManagerResponse]. This function is responsible for creating * the restore key. * @@ -279,24 +279,24 @@ class AuthenticationViewModel @Inject constructor( * @see GenericCredentialManagerResponse */ suspend fun createRestoreKey( - createRestoreKeyOnCredMan: suspend (createRestoreCredRequestObj: JSONObject) -> GenericCredentialManagerResponse, + createRestoreKey: suspend (createRestoreCredentialsObject: JSONObject) -> GenericCredentialManagerResponse, ): Boolean { - return registerPasskey(createRestoreKeyOnCredMan, "Error creating restore key.") + return registerPasskey(createRestoreKey, R.string.error_restore_key) } /** * Conditionally creates a passkey after a successful password login. * - * @param createPasskeyOnCredMan A suspend function that takes a [JSONObject] and returns a + * @param createPasskey A suspend function that takes a [JSONObject] and returns a * [GenericCredentialManagerResponse]. This function is responsible for creating * the passkey. * * @return Boolean indicating success */ suspend fun conditionalCreatePasskey( - createPasskeyOnCredMan: suspend (createPasskeyRequestObj: JSONObject) -> GenericCredentialManagerResponse, + createPasskey: suspend (createPasskeyRequestObject: JSONObject) -> GenericCredentialManagerResponse, ): Boolean { - val success = registerPasskey(createPasskeyOnCredMan, "Error during conditional passkey creation.") + val success = registerPasskey(createPasskey, "Error during conditional passkey creation.") if (success) { repository.setSignedInState(true) } @@ -307,12 +307,12 @@ class AuthenticationViewModel @Inject constructor( * Internal helper to register a passkey (normal or restore key). */ private suspend fun registerPasskey( - createPasskeyOnCredMan: suspend (JSONObject) -> GenericCredentialManagerResponse, + createPasskey: suspend (JSONObject) -> GenericCredentialManagerResponse, errorMessage: String ): Boolean { return when (val result = repository.registerPasskeyCreationRequest()) { is AuthResult.Success -> { - val createPasskeyResponse = createPasskeyOnCredMan(result.data) + val createPasskeyResponse = createPasskey(result.data) if (createPasskeyResponse is GenericCredentialManagerResponse.CreatePasskeySuccess) { repository.registerPasskeyCreationResponse(createPasskeyResponse.createPasskeyResponse) is AuthResult.Success } else { diff --git a/Shrine/app/src/main/res/values/strings.xml b/Shrine/app/src/main/res/values/strings.xml index 298ca57..1da05a1 100644 --- a/Shrine/app/src/main/res/values/strings.xml +++ b/Shrine/app/src/main/res/values/strings.xml @@ -103,6 +103,7 @@ A server error occurred. An unknown error occurred. Invalid credentials. Please check your username and password. + Error creating restore key. Update User Info Update Profile Accept selected credentials From 5880dd91a6cfc22a79c464fa71f7a7d35be15382 Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Thu, 12 Feb 2026 18:31:37 -0500 Subject: [PATCH 4/5] Remove string res addition --- .../shrine/ui/viewmodel/AuthenticationViewModel.kt | 2 +- Shrine/app/src/main/res/values/strings.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt index 527d8fa..d563214 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt @@ -281,7 +281,7 @@ class AuthenticationViewModel @Inject constructor( suspend fun createRestoreKey( createRestoreKey: suspend (createRestoreCredentialsObject: JSONObject) -> GenericCredentialManagerResponse, ): Boolean { - return registerPasskey(createRestoreKey, R.string.error_restore_key) + return registerPasskey(createRestoreKey, "Error creating restore key.") } /** diff --git a/Shrine/app/src/main/res/values/strings.xml b/Shrine/app/src/main/res/values/strings.xml index 1da05a1..298ca57 100644 --- a/Shrine/app/src/main/res/values/strings.xml +++ b/Shrine/app/src/main/res/values/strings.xml @@ -103,7 +103,6 @@ A server error occurred. An unknown error occurred. Invalid credentials. Please check your username and password. - Error creating restore key. Update User Info Update Profile Accept selected credentials From 79ca5f297b7f7377b041485e2874cbd4987aae5f Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Thu, 12 Feb 2026 20:38:12 -0500 Subject: [PATCH 5/5] update function call names --- .../java/com/authentication/shrine/ui/AuthenticationScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt index 9c899ff..70fb65e 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt @@ -76,7 +76,7 @@ fun AuthenticationScreen( val context = LocalContext.current val createRestoreKey: suspend () -> Unit = { viewModel.createRestoreKey( - createRestoreKeyOnCredMan = { createRestoreCredObject -> + createRestoreKey = { createRestoreCredObject -> credentialManagerUtils.createRestoreKey( context = context, requestResult = createRestoreCredObject, @@ -91,7 +91,7 @@ fun AuthenticationScreen( var skipPasskeyPrompt = !isPasswordCredential if (isPasswordCredential) { val conditionalSuccess = viewModel.conditionalCreatePasskey( - createPasskeyOnCredMan = { createPasskeyRequestObj: JSONObject -> + createPasskey = { createPasskeyRequestObj: JSONObject -> credentialManagerUtils.createPasskey( requestResult = createPasskeyRequestObj, context = context,