From a9b7040b8a467971f0662edffb9642b8b8aa5466 Mon Sep 17 00:00:00 2001 From: Neelansh Sahai Date: Fri, 8 Aug 2025 00:18:03 +0530 Subject: [PATCH 1/3] Add Signal API implementation (RP) --- .../shrine/CredentialManagerUtils.kt | 43 ++++++++ .../shrine/ShrineApplication.kt | 6 +- .../shrine/model/PasskeysList.kt | 1 + .../shrine/repository/AuthRepository.kt | 11 ++- .../shrine/ui/MainMenuScreen.kt | 11 +++ .../shrine/ui/PasskeyManagementScreen.kt | 51 +++++++++- .../shrine/ui/SettingsScreen.kt | 2 +- .../shrine/ui/UpdateProfileScreen.kt | 98 +++++++++++++++++++ .../shrine/ui/common/ShrineTextField.kt | 10 ++ .../shrine/ui/navigation/ShrineNavActions.kt | 1 + .../shrine/ui/navigation/ShrineNavGraph.kt | 8 ++ .../viewmodel/PasskeyManagementViewModel.kt | 30 +++++- .../ui/viewmodel/UpdateProfileViewModel.kt | 69 +++++++++++++ Shrine/app/src/main/res/values/strings.xml | 2 + Shrine/gradle/libs.versions.toml | 2 +- Shrine/wear/build.gradle.kts | 17 +++- .../LegacySignInWithGoogleEventListener.kt | 2 +- 17 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt create mode 100644 Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt 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 fd6e4624..2b81214a 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt @@ -33,11 +33,19 @@ import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.GetRestoreCredentialOption +import androidx.credentials.SignalAllAcceptedCredentialIdsRequest +import androidx.credentials.SignalCurrentUserDetailsRequest +import androidx.credentials.SignalUnknownCredentialRequest import androidx.credentials.exceptions.CreateCredentialException import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException import com.authentication.shrine.repository.SERVER_CLIENT_ID import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import com.authentication.shrine.repository.AuthRepository.Companion.RP_ID_KEY +import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY +import com.authentication.shrine.repository.AuthRepository.Companion.read import org.json.JSONObject import javax.inject.Inject @@ -48,6 +56,7 @@ import javax.inject.Inject */ class CredentialManagerUtils @Inject constructor( private val credentialManager: CredentialManager, + private val dataStore: DataStore, ) { /** @@ -249,6 +258,40 @@ class CredentialManagerUtils @Inject constructor( val clearRequest = ClearCredentialStateRequest(requestType = TYPE_CLEAR_RESTORE_CREDENTIAL) credentialManager.clearCredentialState(clearRequest) } + + @SuppressLint("RestrictedApi") + suspend fun signalUnknown( + credentialId: String, + ) { + credentialManager.signalCredentialState( + SignalUnknownCredentialRequest( + """{"rpId":"${dataStore.read(RP_ID_KEY)}", "credentialId":"$credentialId"}""", + ), + ) + } + + @SuppressLint("RestrictedApi") + suspend fun signalAcceptedIds( + credentialIds: List, + ) { + credentialManager.signalCredentialState( + SignalAllAcceptedCredentialIdsRequest( + """{"rpId":"${dataStore.read(RP_ID_KEY)}","userId":"${dataStore.read(USER_ID_KEY)}","allAcceptedCredentialIds":["${credentialIds.joinToString(",")}"]}""", + ), + ) + } + + @SuppressLint("RestrictedApi") + suspend fun signalUserDetails( + newName: String, + newDisplayName: String, + ) { + credentialManager.signalCredentialState( + SignalCurrentUserDetailsRequest( + """{"rpId":"${dataStore.read(RP_ID_KEY)}","userId":"${dataStore.read(USER_ID_KEY)}", "name":"$newName","displayName":"$newDisplayName"}""", + ), + ) + } } sealed class GenericCredentialManagerResponse { diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt b/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt index dac86d1e..1a751f23 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt @@ -109,8 +109,12 @@ object AppModule { @Provides fun providesCredentialManagerUtils( credentialManager: CredentialManager, + dataStore: DataStore, ): CredentialManagerUtils { - return CredentialManagerUtils(credentialManager) + return CredentialManagerUtils( + credentialManager = credentialManager, + dataStore = dataStore, + ) } @Singleton diff --git a/Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt index a2656cf0..4e448183 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt @@ -47,4 +47,5 @@ data class PasskeyCredential( val aaguid: String, val registeredAt: Long, val providerIcon: String, + val isSelected: Boolean = false, ) 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 e355d3f3..53fe9309 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 @@ -78,6 +78,9 @@ class AuthRepository @Inject constructor( val USERNAME = stringPreferencesKey("username") val IS_SIGNED_IN_THROUGH_PASSKEYS = booleanPreferencesKey("is_signed_passkeys") val SESSION_ID = stringPreferencesKey("session_id") + val RP_ID_KEY = stringPreferencesKey("rp_id_key") + val USER_ID_KEY = stringPreferencesKey("user_id_key") + val CRED_ID = stringPreferencesKey("cred_id") val RESTORE_KEY_CREDENTIAL_ID = stringPreferencesKey("restore_key_credential_id") // Value for restore credential AuthApiService parameter @@ -151,7 +154,6 @@ class AuthRepository @Inject constructor( } } - /** * Signs in with a password. * @@ -217,6 +219,7 @@ class AuthRepository @Inject constructor( ) if (response.isSuccessful) { dataStore.edit { prefs -> + prefs[RP_ID_KEY] = response.body()?.rp?.id ?: "" response.getSessionId()?.also { prefs[SESSION_ID] = it } @@ -287,6 +290,7 @@ class AuthRepository @Inject constructor( ) if (apiResult.isSuccessful) { dataStore.edit { prefs -> + prefs[CRED_ID] = rawId if (credentialResponse is CreateRestoreCredentialResponse) { prefs[RESTORE_KEY_CREDENTIAL_ID] = rawId } @@ -321,6 +325,7 @@ class AuthRepository @Inject constructor( val response = authApiService.signInRequest() if (response.isSuccessful) { dataStore.edit { prefs -> + prefs[RP_ID_KEY] = response.body()?.rpId ?: "" response.getSessionId()?.also { prefs[SESSION_ID] = it } @@ -382,6 +387,7 @@ class AuthRepository @Inject constructor( ) return if (apiResult.isSuccessful) { dataStore.edit { prefs -> + prefs[CRED_ID] = credentialId apiResult.getSessionId()?.also { prefs[SESSION_ID] = it } @@ -545,6 +551,9 @@ class AuthRepository @Inject constructor( cookie = sessionId.createCookieHeader(), ) if (apiResult.isSuccessful) { + dataStore.edit { prefs -> + prefs[USER_ID_KEY] = apiResult.body()?.userId ?: "" + } return apiResult.body() } else if (apiResult.code() == 401) { signOut() diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt index 821cac4f..138c8986 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt @@ -55,6 +55,7 @@ fun MainMenuScreen( onSettingsButtonClicked: () -> Unit, onHelpButtonClicked: () -> Unit, navigateToLogin: () -> Unit, + navigateToUpdateProfile: () -> Unit, viewModel: HomeViewModel, modifier: Modifier = Modifier, credentialManagerUtils: CredentialManagerUtils, @@ -71,6 +72,7 @@ fun MainMenuScreen( onHelpButtonClicked = onHelpButtonClicked, navigateToLogin = navigateToLogin, onSignOut = onSignOut, + navigateToUpdateProfile = navigateToUpdateProfile, modifier = modifier, ) } @@ -93,6 +95,7 @@ fun MainMenuScreen( onSettingsButtonClicked: () -> Unit, onHelpButtonClicked: () -> Unit, navigateToLogin: () -> Unit, + navigateToUpdateProfile: () -> Unit, onSignOut: () -> Unit, modifier: Modifier = Modifier, ) { @@ -121,6 +124,7 @@ fun MainMenuScreen( onHelpButtonClicked, onSignOut, navigateToLogin, + navigateToUpdateProfile, ) } } @@ -142,6 +146,7 @@ private fun MainMenuButtonsList( onHelpButtonClicked: () -> Unit, onSignOut: () -> Unit, navigateToLogin: () -> Unit, + navigateToUpdateProfile: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -172,6 +177,11 @@ private fun MainMenuButtonsList( buttonText = stringResource(R.string.sign_out), usePrimaryColor = false, ) + + ShrineButton( + onClick = navigateToUpdateProfile, + buttonText = "Update Profile", + ) } } @@ -188,6 +198,7 @@ fun PasskeysSignedPreview() { onHelpButtonClicked = { }, navigateToLogin = { }, onSignOut = { }, + navigateToUpdateProfile = { }, ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt index 7cb70829..3906cb07 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Checkbox import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -94,7 +96,22 @@ fun PasskeyManagementScreen( requestResult = data, context = context, ) - }) + }, + ) + } + } + + val passkeysList = uiState.passkeysList + val onItemClick = { index: Int -> + viewModel.updateItem(index, passkeysList) + } + + val onSignalBtnClicked = { + val credentialIdsList = passkeysList + .filter { it.isSelected } + .map { it.id } + if (credentialIdsList.isNotEmpty()) { + viewModel.signalAccepted(credentialIdsList) } } @@ -104,8 +121,10 @@ fun PasskeyManagementScreen( onCreatePasskeyClicked = onCreatePasskeyClicked, onDeleteClicked = onDeleteClicked, uiState = uiState, - passkeysList = uiState.passkeysList, + passkeysList = passkeysList, aaguidData = uiState.aaguidData, + onItemClick = onItemClick, + onSignal = onSignalBtnClicked, modifier = modifier, ) } @@ -126,6 +145,8 @@ fun PasskeyManagementScreen( uiState: PasskeyManagementUiState, passkeysList: List, aaguidData: Map>, + onItemClick: (Int) -> Unit, + onSignal: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = remember { SnackbarHostState() } @@ -161,6 +182,13 @@ fun PasskeyManagementScreen( onDeleteClicked = onDeleteClicked, passkeysList = passkeysList, aaguidData = aaguidData, + onItemClick = onItemClick, + ) + + ShrineButton( + onClick = onSignal, + buttonText = "Signal Accepted", + modifier = Modifier.fillMaxWidth() ) } else { ShrineButton( @@ -185,7 +213,7 @@ fun PasskeyManagementScreen( if (snackbarMessage != null) { LaunchedEffect(snackbarMessage) { snackbarHostState.showSnackbar( - message = snackbarMessage + message = snackbarMessage, ) } } @@ -202,6 +230,7 @@ fun PasskeysListColumn( onDeleteClicked: (credentialId: String) -> Unit, passkeysList: List, aaguidData: Map>, + onItemClick: (Int) -> Unit, ) { val shape = RoundedCornerShape(dimensionResource(R.dimen.padding_small)) LazyColumn( @@ -222,13 +251,15 @@ fun PasskeysListColumn( iconSvgString = aaguidData[item.aaguid]?.get("icon_light"), credentialProviderName = item.name, passkeyCreationDate = item.registeredAt.toReadableDate(), + isChecked = item.isSelected, + modifier = Modifier.clickable { onItemClick(index) } ) if (index < passkeysList.lastIndex) { HorizontalDivider( modifier = Modifier.padding( vertical = dimensionResource(R.dimen.padding_extra_small), - horizontal = dimensionResource(R.dimen.dimen_standard) + horizontal = dimensionResource(R.dimen.dimen_standard), ), thickness = 1.dp, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -253,9 +284,11 @@ fun PasskeysDetailsRow( iconSvgString: String?, credentialProviderName: String, passkeyCreationDate: String, + isChecked: Boolean, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = dimensionResource(R.dimen.padding_small)), verticalAlignment = Alignment.CenterVertically, @@ -268,6 +301,12 @@ fun PasskeysDetailsRow( .build(), ) + Checkbox( + checked = isChecked, + onCheckedChange = {}, + modifier = Modifier.clickable(enabled = false, onClick = { }) + ) + Image( modifier = Modifier.size(48.dp), painter = painter, @@ -332,6 +371,8 @@ fun PasskeyManagementScreenPreview() { ) ), aaguidData = mapOf(), + onItemClick = { _ -> }, + onSignal = { }, ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/SettingsScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/SettingsScreen.kt index cff54845..a81f081b 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/SettingsScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/SettingsScreen.kt @@ -171,7 +171,7 @@ fun SettingsScreen( if (snackbarMessage != null) { LaunchedEffect(snackbarMessage) { snackbarHostState.showSnackbar( - message = snackbarMessage + message = snackbarMessage, ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt new file mode 100644 index 00000000..897b9019 --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package com.authentication.shrine.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.authentication.shrine.R +import com.authentication.shrine.ui.common.ShrineButton +import com.authentication.shrine.ui.common.ShrineTextField +import com.authentication.shrine.ui.common.ShrineToolbar +import com.authentication.shrine.ui.theme.ShrineTheme +import com.authentication.shrine.ui.viewmodel.UpdateProfileViewModel + +@Composable +fun UpdateProfileScreen( + viewModel: UpdateProfileViewModel, +) { + var username by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + + UpdateProfileScreen( + username = username, + onUsernameChanged = { username = it }, + email = email, + onEmailChanged = { email = it }, + onMetadataUpdate = viewModel::updateMetadata, + ) +} + +@Composable +fun UpdateProfileScreen( + username: String, + onUsernameChanged: (String) -> Unit, + email: String, + onEmailChanged: (String) -> Unit, + onMetadataUpdate: (String, String) -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + ShrineToolbar(onBackClicked = {}) + + ShrineTextField( + value = username, + onValueChange = onUsernameChanged, + hint = stringResource(R.string.username), + modifier = Modifier.fillMaxWidth() + ) + + ShrineTextField( + value = email, + onValueChange = onEmailChanged, + hint = stringResource(R.string.email_address), + modifier = Modifier.fillMaxWidth() + ) + + ShrineButton( + onClick = { onMetadataUpdate(username, email) }, + buttonText = stringResource(R.string.update_user_info), + ) + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +fun TestPreview() { + ShrineTheme { + UpdateProfileScreen( + "", + {}, + "", + {}, + {_, _ -> }, + ) + } +} diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt index 65c8a8ca..fdce2c9f 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt @@ -32,6 +32,13 @@ import androidx.compose.ui.tooling.preview.Preview import com.authentication.shrine.R import com.authentication.shrine.ui.theme.ShrineTheme +/** + * A custom TextField composable for the Shrine app. + * + * @param modifier The modifier to be applied to the TextField. + * @param value The current value of the TextField. + * @param onValueChange The callback to be invoked when the TextField value changes. + */ @Composable fun ShrineTextField( title: String, @@ -62,6 +69,9 @@ fun ShrineTextField( } } +/** + * A preview of the ShrineTextField composable. + */ @Preview(showSystemUi = true, name = "ShrineTextField Light") @Composable fun ShrineTextFieldPreviewLight() { 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 66369fc6..28a5c2cc 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 @@ -38,6 +38,7 @@ enum class ShrineAppDestinations(@StringRes val title: Int) { NavHostRoute(title = R.string.nav_host_route), PasskeyManagementTab(title = R.string.passkey_management), OtherOptionsSignInRoute(title = R.string.other_ways_to_sign_in), + UpdateProfileRoute(title = R.string.update_profile), } /** diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt index 88b65c9f..f601e7df 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt @@ -35,6 +35,7 @@ import com.authentication.shrine.ui.RegisterPasswordScreen import com.authentication.shrine.ui.RegisterScreen import com.authentication.shrine.ui.SettingsScreen import com.authentication.shrine.ui.ShrineAppScreen +import com.authentication.shrine.ui.UpdateProfileScreen /** * The navigation graph for the Shrine app. @@ -93,6 +94,7 @@ fun ShrineNavGraph( navigateToLogin = navigateToLogin, viewModel = hiltViewModel(), credentialManagerUtils = credentialManagerUtils, + navigateToUpdateProfile = { navController.navigate(ShrineAppDestinations.UpdateProfileRoute.name) }, ) } @@ -176,5 +178,11 @@ fun ShrineNavGraph( composable(route = ShrineAppDestinations.ShrineApp.name) { ShrineAppScreen() } + + composable(route = ShrineAppDestinations.UpdateProfileRoute.name) { + UpdateProfileScreen( + viewModel = hiltViewModel(), + ) + } } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt index 11e81e8b..64cf3483 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt @@ -49,7 +49,8 @@ import javax.inject.Inject @HiltViewModel class PasskeyManagementViewModel @Inject constructor( private val authRepository: AuthRepository, - private val application: Application + private val application: Application, + private val credentialManagerUtils: CredentialManagerUtils, ) : ViewModel() { private val _uiState = MutableStateFlow(PasskeyManagementUiState()) val uiState = _uiState.asStateFlow() @@ -70,7 +71,7 @@ class PasskeyManagementViewModel @Inject constructor( val reader = InputStreamReader(aaguidInputStream) val aaguidJsonData = gson.fromJson>>( reader, - object : TypeToken>>() {}.type + object : TypeToken>>() {}.type, ) _uiState.update { it.copy(aaguidData = aaguidJsonData) } } catch (e: Exception) { @@ -167,7 +168,7 @@ class PasskeyManagementViewModel @Inject constructor( _uiState.update { it.copy( isLoading = false, - errorMessage = createPasskeyResponse.errorMessage + errorMessage = createPasskeyResponse.errorMessage, ) } authRepository.setSignedInState(false) @@ -214,6 +215,7 @@ class PasskeyManagementViewModel @Inject constructor( } viewModelScope.launch { + credentialManagerUtils.signalUnknown(credentialId) when (val result = authRepository.deletePasskey(credentialId)) { is AuthResult.Success -> { // Refresh passkeys list after deleting a passkey @@ -226,7 +228,7 @@ class PasskeyManagementViewModel @Inject constructor( isLoading = false, userHasPasskeys = filteredPasskeysList.isNotEmpty(), passkeysList = filteredPasskeysList, - messageResourceId = R.string.delete_passkey_successful + messageResourceId = R.string.delete_passkey_successful, ) } } else { @@ -266,6 +268,26 @@ class PasskeyManagementViewModel @Inject constructor( } } } + + fun signalAccepted(credentialsIds: List) { + viewModelScope.launch { + credentialManagerUtils.signalAcceptedIds(credentialsIds) + } + } + + fun updateItem(index: Int, passkeysList: List) { + _uiState.update { + it.copy( + passkeysList = passkeysList.mapIndexed { clickIndex, passkeyCredential -> + if (clickIndex == index) { + passkeyCredential.copy(isSelected = !passkeyCredential.isSelected) + } else { + passkeyCredential + } + } + ) + } + } } /** diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt new file mode 100644 index 00000000..f525bf99 --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package com.authentication.shrine.ui.viewmodel + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.lifecycle.ViewModel +import com.authentication.shrine.CredentialManagerUtils +import com.authentication.shrine.repository.AuthRepository.Companion.CRED_ID +import com.authentication.shrine.repository.AuthRepository.Companion.RP_ID_KEY +import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY +import com.authentication.shrine.repository.AuthRepository.Companion.read +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 +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class UpdateProfileViewModel @Inject constructor( + private val credentialManagerUtils: CredentialManagerUtils, + private val dataStore: DataStore, + private val coroutineScope: CoroutineScope, +) : ViewModel() { + private val _uiState = MutableStateFlow(TestState()) + val uiState = _uiState.asStateFlow() + + init { + coroutineScope.launch { + _uiState.update { + TestState( + userId = dataStore.read(USER_ID_KEY) ?: "", + rpId = dataStore.read(RP_ID_KEY) ?: "", + credentialId = dataStore.read(CRED_ID) ?: "", + ) + } + } + } + + fun updateMetadata( + newName: String, + newDisplayName: String, + ) { + coroutineScope.launch { + credentialManagerUtils.signalUserDetails(newName, newDisplayName) + } + } +} + +data class TestState( + val userId: String = "", + val rpId: String = "", + val credentialId: String = "", +) diff --git a/Shrine/app/src/main/res/values/strings.xml b/Shrine/app/src/main/res/values/strings.xml index 34838c5e..665ffe83 100644 --- a/Shrine/app/src/main/res/values/strings.xml +++ b/Shrine/app/src/main/res/values/strings.xml @@ -102,4 +102,6 @@ An unexpected server error occurred. An unknown error occurred. Invalid credentials. Please check your username and password. + Update User Info + Update Profile diff --git a/Shrine/gradle/libs.versions.toml b/Shrine/gradle/libs.versions.toml index 26b7a696..7f3aac97 100644 --- a/Shrine/gradle/libs.versions.toml +++ b/Shrine/gradle/libs.versions.toml @@ -6,7 +6,7 @@ browser = "1.8.0" coil = "2.7.0" coilSvg = "2.6.0" coreSplashscreen = "1.0.1" -credentials = "1.5.0" +credentials = "1.6.0-alpha05" datastorePrefs = "1.1.1" googleFonts = "1.6.8" googleServicesPlugin = "4.4.1" diff --git a/Shrine/wear/build.gradle.kts b/Shrine/wear/build.gradle.kts index b97873cb..77b36b24 100644 --- a/Shrine/wear/build.gradle.kts +++ b/Shrine/wear/build.gradle.kts @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -105,4 +120,4 @@ android { // For Legacy Sign in With Google implementation(libs.play.services.auth) // 21.1.1 -> 21.3.0 } -} \ No newline at end of file +} diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/LegacySignInWithGoogleEventListener.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/LegacySignInWithGoogleEventListener.kt index 585faedd..43390dec 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/LegacySignInWithGoogleEventListener.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/LegacySignInWithGoogleEventListener.kt @@ -34,7 +34,7 @@ object LegacySignInWithGoogleEventListener : GoogleSignInEventListener { "Legacy Google Account received: %s. Registering to application credential repository" private const val ERROR_MISSING_ID_TOKEN = "Signed in, but failed to register Legacy Google sign in account to application repository due to missing Google Sign in idToken. " + - "Verify OAuthClient type is 'web' and that GoogleSignInOptionsBuilder.requestIdToken is passed the correct client id." + "Verify OAuthClient type is 'web' and that GoogleSignInOptionsBuilder.requestIdToken is passed the correct client id." /** * Called when a Google Sign-In is successful and a [GoogleSignInAccount] is obtained. From 4c67e30c9278f3ec8b956f61ad09ebf268892a53 Mon Sep 17 00:00:00 2001 From: Neelansh Sahai <96shubh@gmail.com> Date: Thu, 28 Aug 2025 12:15:02 +0530 Subject: [PATCH 2/3] Add spotless fixes --- .../shrine/CredentialManagerUtils.kt | 6 +-- .../shrine/api/AuthApiService.kt | 2 +- .../shrine/model/FederationOptionsRequest.kt | 19 ++++++++- .../shrine/model/SignInWithGoogleRequest.kt | 19 ++++++++- .../shrine/repository/AuthRepository.kt | 11 +++++- .../shrine/ui/AuthenticationScreen.kt | 16 +++++++- .../shrine/ui/PasskeyManagementScreen.kt | 22 ++++++++--- .../shrine/ui/UpdateProfileScreen.kt | 6 +-- .../shrine/ui/common/ShrineTextField.kt | 4 ++ .../viewmodel/PasskeyManagementViewModel.kt | 2 +- .../com/authentication/shrinewear/Graph.kt | 6 +-- .../authenticator/AuthenticationServer.kt | 39 ++++++++++++------- .../CredentialManagerAuthenticator.kt | 6 +-- .../shrinewear/extensions/OkHttpExtensions.kt | 19 ++++++++- .../extensions/PasskeyJsonHelpers.kt | 17 +++++++- .../shrinewear/network/AuthNetworkClient.kt | 28 ++++++++++--- .../authentication/shrinewear/ui/ShrineApp.kt | 2 +- .../shrinewear/ui/screens/HomeScreen.kt | 1 - .../shrinewear/ui/screens/SignOutScreen.kt | 2 - .../viewmodel/CredentialManagerViewModel.kt | 2 +- 20 files changed, 175 insertions(+), 54 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 2b81214a..1763920b 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt @@ -39,13 +39,13 @@ import androidx.credentials.SignalUnknownCredentialRequest import androidx.credentials.exceptions.CreateCredentialException import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException -import com.authentication.shrine.repository.SERVER_CLIENT_ID -import com.google.android.libraries.identity.googleid.GetGoogleIdOption import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.authentication.shrine.repository.AuthRepository.Companion.RP_ID_KEY import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY import com.authentication.shrine.repository.AuthRepository.Companion.read +import com.authentication.shrine.repository.SERVER_CLIENT_ID +import com.google.android.libraries.identity.googleid.GetGoogleIdOption import org.json.JSONObject import javax.inject.Inject @@ -117,7 +117,7 @@ class CredentialManagerUtils @Inject constructor( .setServerClientId(SERVER_CLIENT_ID) .setFilterByAuthorizedAccounts(false) .build(), - ) + ), ) result = credentialManager.getCredential(context, credentialRequest) } catch (e: GetCredentialCancellationException) { 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 dfef9b32..406339c7 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 @@ -165,7 +165,7 @@ interface AuthApiService { */ @POST("federation/options") suspend fun getFederationOptions( - @Body urls: FederationOptionsRequest + @Body urls: FederationOptionsRequest, ): Response /** diff --git a/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt index 16f2d6a3..254a17db 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ package com.authentication.shrine.model /** @@ -5,5 +20,5 @@ package com.authentication.shrine.model * @param urls a list of urls to send for federated requests. */ data class FederationOptionsRequest( - val urls: List = listOf("https://accounts.google.com") -) \ No newline at end of file + val urls: List = listOf("https://accounts.google.com"), +) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt index 9ae7d1f6..ba38bc0c 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ package com.authentication.shrine.model /** @@ -7,5 +22,5 @@ package com.authentication.shrine.model */ data class SignInWithGoogleRequest( val token: String, - val url: String = "https://accounts.google.com" -) \ No newline at end of file + val url: String = "https://accounts.google.com", +) 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 53fe9309..eed1354f 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 @@ -427,15 +427,22 @@ class AuthRepository @Inject constructor( */ suspend fun signInWithFederatedTokenResponse( sessionId: String, +<<<<<<< HEAD credentialResponse: GetCredentialResponse ): AuthResult { return try { val credential = credentialResponse.credential +======= + credentialResponse: GetCredentialResponse, + ): Boolean { + val credential = credentialResponse.credential + try { +>>>>>>> b12d692 (Add spotless fixes) if (credential is CustomCredential) { val isSuccess = verifyIdToken( sessionId, GoogleIdTokenCredential - .createFrom(credential.data).idToken + .createFrom(credential.data).idToken, ) if (isSuccess) { AuthResult.Success(Unit) @@ -643,7 +650,7 @@ class AuthRepository @Inject constructor( suspend fun verifyIdToken(sessionId: String, token: String): Boolean { val apiResult = authApiService.verifyIdToken( cookie = sessionId.createCookieHeader(), - requestParams = SignInWithGoogleRequest(token = token) + requestParams = SignInWithGoogleRequest(token = token), ) if (apiResult.isSuccessful) { 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 9937f2dd..99dccbcb 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 @@ -190,6 +190,7 @@ fun AuthenticationScreen( isButtonEnabled = !uiState.isLoading, ) +<<<<<<< HEAD // Sign in with Google image Image( painter = painterResource(id = R.drawable.siwg_button_light), @@ -201,6 +202,19 @@ fun AuthenticationScreen( onClick = onSignInWithSignInWithGoogleRequest) ) } +======= + // Sign in with Google image + Image( + painter = painterResource(id = R.drawable.siwg_button_light), + contentDescription = stringResource(R.string.sign_in_with_google_button), + modifier = Modifier + .height(dimensionResource(R.dimen.siwg_button_height)) + .clickable( + enabled = !uiState.isLoading, + onClick = onSignInWithSignInWithGoogleRequest, + ), + ) +>>>>>>> b12d692 (Add spotless fixes) } if (uiState.isLoading) { @@ -229,7 +243,7 @@ fun AuthenticationScreen( if (snackbarMessage != null) { LaunchedEffect(snackbarMessage) { snackbarHostState.showSnackbar( - message = snackbarMessage + message = snackbarMessage, ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt index 3906cb07..568f1a48 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt @@ -42,6 +42,10 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +<<<<<<< HEAD +======= +import androidx.compose.runtime.getValue +>>>>>>> b12d692 (Add spotless fixes) import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -108,7 +112,7 @@ fun PasskeyManagementScreen( val onSignalBtnClicked = { val credentialIdsList = passkeysList - .filter { it.isSelected } + .filter { !it.isSelected } .map { it.id } if (credentialIdsList.isNotEmpty()) { viewModel.signalAccepted(credentialIdsList) @@ -187,8 +191,8 @@ fun PasskeyManagementScreen( ShrineButton( onClick = onSignal, - buttonText = "Signal Accepted", - modifier = Modifier.fillMaxWidth() + buttonText = "Delete selected credentials", + modifier = Modifier.fillMaxWidth(), ) } else { ShrineButton( @@ -252,7 +256,7 @@ fun PasskeysListColumn( credentialProviderName = item.name, passkeyCreationDate = item.registeredAt.toReadableDate(), isChecked = item.isSelected, - modifier = Modifier.clickable { onItemClick(index) } + modifier = Modifier.clickable { onItemClick(index) }, ) if (index < passkeysList.lastIndex) { @@ -304,7 +308,7 @@ fun PasskeysDetailsRow( Checkbox( checked = isChecked, onCheckedChange = {}, - modifier = Modifier.clickable(enabled = false, onClick = { }) + modifier = Modifier.clickable(enabled = false, onClick = { }), ) Image( @@ -370,10 +374,18 @@ fun PasskeyManagementScreenPreview() { providerIcon = "" ) ), +<<<<<<< HEAD aaguidData = mapOf(), onItemClick = { _ -> }, onSignal = { }, ) } +======= + ), + aaguidData = mapOf(), + { _ -> }, + {}, + ) +>>>>>>> b12d692 (Add spotless fixes) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt index 897b9019..c7386f1d 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt @@ -66,14 +66,14 @@ fun UpdateProfileScreen( value = username, onValueChange = onUsernameChanged, hint = stringResource(R.string.username), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) ShrineTextField( value = email, onValueChange = onEmailChanged, hint = stringResource(R.string.email_address), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) ShrineButton( @@ -92,7 +92,7 @@ fun TestPreview() { {}, "", {}, - {_, _ -> }, + { _, _ -> }, ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt index fdce2c9f..f29298f5 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt @@ -28,6 +28,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +<<<<<<< HEAD +======= +import androidx.compose.ui.res.stringResource +>>>>>>> b12d692 (Add spotless fixes) import androidx.compose.ui.tooling.preview.Preview import com.authentication.shrine.R import com.authentication.shrine.ui.theme.ShrineTheme diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt index 64cf3483..0adf6384 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt @@ -284,7 +284,7 @@ class PasskeyManagementViewModel @Inject constructor( } else { passkeyCredential } - } + }, ) } } diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/Graph.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/Graph.kt index cda6fb98..6a74c437 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/Graph.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/Graph.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow - /** * Represents the possible authentication states of the application. */ @@ -65,8 +64,8 @@ object Graph { lateinit var credentialManagerAuthenticator: CredentialManagerAuthenticator private set - private val _authenticationState = MutableStateFlow(AuthenticationState.LOGGED_OUT) + /** * Stores the current authentication status code. Defaults to [AuthenticationState.LOGGED_OUT]. */ @@ -83,7 +82,8 @@ object Graph { fun provide(context: Context) { credentialManagerAuthenticator = CredentialManagerAuthenticator( context, - authenticationServer) + authenticationServer, + ) } fun updateAuthenticationState(newState: AuthenticationState) { diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/AuthenticationServer.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/AuthenticationServer.kt index 3c5eed04..61d20001 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/AuthenticationServer.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/AuthenticationServer.kt @@ -54,8 +54,10 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { * if the server indicates a sign-out state or an error occurs during retrieval. */ internal suspend fun getPublicKeyRequestOptions(): String { - return when (val publicKeyRequestOptions = - authNetworkClient.fetchPublicKeyRequestOptions()) { + return when ( + val publicKeyRequestOptions = + authNetworkClient.fetchPublicKeyRequestOptions() + ) { is NetworkResult.Success -> { publicKeyRequestOptions.sessionId?.let { newSessionId -> sessionId = newSessionId @@ -78,9 +80,12 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { * @return `true` on successful login and session update, `false` on failure. */ internal suspend fun loginWithPasskey(passkeyResponseJSON: String): Boolean { - return when (val authorizationResult = authNetworkClient.authorizePasskeyWithServer( - passkeyResponseJSON, sessionId - )) { + return when ( + val authorizationResult = authNetworkClient.authorizePasskeyWithServer( + passkeyResponseJSON, + sessionId, + ) + ) { is NetworkResult.Success -> { authorizationResult.sessionId?.let { newSessionId -> sessionId = newSessionId @@ -115,7 +120,7 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { is NetworkResult.SignedOutFromServer -> { signOut() - Log.e(TAG, "Username ${username} not found in server") + Log.e(TAG, "Username $username not found in server") return false } } @@ -126,8 +131,10 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { return false } - return when (val result = - authNetworkClient.authorizePasswordWithServer(usernameSessionId, password)) { + return when ( + val result = + authNetworkClient.authorizePasswordWithServer(usernameSessionId, password) + ) { is NetworkResult.Success -> { result.sessionId?.let { passwordSessionId -> sessionId = passwordSessionId @@ -137,7 +144,7 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { is NetworkResult.SignedOutFromServer -> { signOut() - Log.e(TAG, "Password: ${password} incorrect") + Log.e(TAG, "Password: $password incorrect") sessionId = null false } @@ -156,7 +163,7 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { if (type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { federatedToken = GoogleIdTokenCredential.createFrom(data).idToken } else { - Log.e(TAG, "Unrecognized custom credential: ${type}") + Log.e(TAG, "Unrecognized custom credential: $type") return false } @@ -190,11 +197,13 @@ class AuthenticationServer(private val authNetworkClient: AuthNetworkClient) { } } - return when (val authorizationResult = - authNetworkClient.authorizeFederatedTokenWithServer( - federatedToken, - federatedSessionId - )) { + return when ( + val authorizationResult = + authNetworkClient.authorizeFederatedTokenWithServer( + federatedToken, + federatedSessionId, + ) + ) { is NetworkResult.Success -> { this.sessionId = authorizationResult.sessionId return true diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/CredentialManagerAuthenticator.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/CredentialManagerAuthenticator.kt index bc20f8ad..e2efbba6 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/CredentialManagerAuthenticator.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/authenticator/CredentialManagerAuthenticator.kt @@ -45,7 +45,7 @@ import com.google.android.libraries.identity.googleid.GetGoogleIdOption */ class CredentialManagerAuthenticator( applicationContext: Context, - private val authenticationServer: AuthenticationServer + private val authenticationServer: AuthenticationServer, ) { private val credentialManager: CredentialManager = CredentialManager.create(applicationContext) @@ -96,14 +96,14 @@ class CredentialManagerAuthenticator( is PasswordCredential -> { return authenticationServer.loginWithPassword( credential.id, - credential.password + credential.password, ) } is CustomCredential -> { return authenticationServer.loginWithCustomCredential( credential.type, - credential.data + credential.data, ) } diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/OkHttpExtensions.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/OkHttpExtensions.kt index f0223f63..f4adf0a0 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/OkHttpExtensions.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/OkHttpExtensions.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ package com.authentication.shrinewear.extensions import android.util.JsonReader @@ -50,7 +65,7 @@ suspend fun Call.await(): Response { */ fun Response.result( errorMessage: String, - data: Response.() -> T + data: Response.() -> T, ): NetworkResult { if (!isSuccessful) { if (code == 401) { // Unauthorized @@ -125,4 +140,4 @@ private fun parseSessionId(cookie: String): String { val semicolon = cookie.indexOf(";", start + SESSION_ID_KEY.length) val end = if (semicolon < 0) cookie.length else semicolon return cookie.substring(start + SESSION_ID_KEY.length, end) -} \ No newline at end of file +} diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/PasskeyJsonHelpers.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/PasskeyJsonHelpers.kt index 98d36d59..61ddd4a2 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/PasskeyJsonHelpers.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/extensions/PasskeyJsonHelpers.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ package com.authentication.shrinewear.extensions import android.util.Base64 @@ -133,4 +148,4 @@ private fun parseCredentialDescriptors( private fun b64Decode(str: String): ByteArray { return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE) -} \ No newline at end of file +} diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/network/AuthNetworkClient.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/network/AuthNetworkClient.kt index 3b1b27c2..41a4e02d 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/network/AuthNetworkClient.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/network/AuthNetworkClient.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ package com.authentication.shrinewear.network import android.os.Build @@ -30,7 +45,7 @@ class AuthNetworkClient { init { val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} " + - "(Android ${Build.VERSION.RELEASE}; ${Build.MODEL}; ${Build.BRAND})" + "(Android ${Build.VERSION.RELEASE}; ${Build.MODEL}; ${Build.BRAND})" httpClient = OkHttpClient.Builder() .addInterceptor(NetworkAddHeaderInterceptor(userAgent)) .addInterceptor( @@ -146,7 +161,8 @@ class AuthNetworkClient { "POST", createJSONRequestBody { name("urls").beginArray().value("https://accounts.google.com").endArray() - }).build(), + }, + ).build(), ).await() return httpResponse.result(errorMessage = "Error creating federation options") {} @@ -163,10 +179,12 @@ class AuthNetworkClient { * A [Unit] type for success implies no specific data is returned on successful authorization. */ internal suspend fun authorizeFederatedTokenWithServer( - token: String, sessionId: String + token: String, + sessionId: String, ): NetworkResult { val requestHeaders = okhttp3.Headers.Builder().add( - "Cookie", "$SESSION_ID_KEY$sessionId" + "Cookie", + "$SESSION_ID_KEY$sessionId", ).build() val httpResponse = httpClient.newCall( @@ -183,4 +201,4 @@ class AuthNetworkClient { return httpResponse.result(errorMessage = "Error signing in with the federated token") { } } -} \ No newline at end of file +} diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/ShrineApp.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/ShrineApp.kt index 03b7337e..2a75cbfd 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/ShrineApp.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/ShrineApp.kt @@ -33,4 +33,4 @@ fun ShrineApp() { val navigationActions = remember(navController) { ShrineNavActions(navController) } ShrineNavGraph(navController = navController, navigationActions = navigationActions) -} \ No newline at end of file +} diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/HomeScreen.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/HomeScreen.kt index d6c70718..c143bc2b 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/HomeScreen.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/HomeScreen.kt @@ -78,7 +78,6 @@ fun HomeScreen( } } - object DemoInstructionsState { var isFirstLaunch: Boolean = true } diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/SignOutScreen.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/SignOutScreen.kt index 8a740c0c..eb416fe2 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/SignOutScreen.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/screens/SignOutScreen.kt @@ -25,9 +25,7 @@ import androidx.wear.compose.material3.AlertDialog import androidx.wear.compose.material3.AlertDialogDefaults import androidx.wear.compose.material3.Text import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices -import com.authentication.shrinewear.AuthenticationState import com.authentication.shrinewear.Graph -import com.authentication.shrinewear.R /** * Composable screen displayed after a successful sign-in, allowing the user to sign out. diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/CredentialManagerViewModel.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/CredentialManagerViewModel.kt index 5e6811dd..30407193 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/CredentialManagerViewModel.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/viewmodel/CredentialManagerViewModel.kt @@ -54,7 +54,7 @@ class CredentialManagerViewModel : ViewModel() { } catch (e: GetCredentialCancellationException) { Log.i( TAG, - "Dismissed, launching old authentication. Exception: %s".format(e.message) + "Dismissed, launching old authentication. Exception: %s".format(e.message), ) Graph.updateAuthenticationState(AuthenticationState.DISMISSED_BY_USER) } catch (_: NoCredentialException) { From e93bf42eabd09d7cd80a1f2856f90d7379110a16 Mon Sep 17 00:00:00 2001 From: Neelansh Sahai <96shubh@gmail.com> Date: Thu, 18 Sep 2025 23:58:07 +0530 Subject: [PATCH 3/3] Fix code review changes --- .../shrine/CredentialManagerUtils.kt | 25 +++++++-- .../shrine/ui/MainMenuScreen.kt | 2 +- .../shrine/ui/PasskeyManagementScreen.kt | 5 +- .../shrine/ui/UpdateProfileScreen.kt | 55 ++++++++++++++----- .../shrine/ui/navigation/ShrineNavGraph.kt | 1 + .../viewmodel/PasskeyManagementViewModel.kt | 3 + .../ui/viewmodel/UpdateProfileViewModel.kt | 34 +++++++++--- Shrine/app/src/main/res/values/strings.xml | 2 + 8 files changed, 96 insertions(+), 31 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 1763920b..5a3ad098 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt @@ -46,6 +46,7 @@ import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY import com.authentication.shrine.repository.AuthRepository.Companion.read import com.authentication.shrine.repository.SERVER_CLIENT_ID import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import org.json.JSONArray import org.json.JSONObject import javax.inject.Inject @@ -264,8 +265,11 @@ class CredentialManagerUtils @Inject constructor( credentialId: String, ) { credentialManager.signalCredentialState( - SignalUnknownCredentialRequest( - """{"rpId":"${dataStore.read(RP_ID_KEY)}", "credentialId":"$credentialId"}""", + request = SignalUnknownCredentialRequest( + requestJson = JSONObject().apply { + put("rpId", dataStore.read(RP_ID_KEY)) + put("credentialId", credentialId) + }.toString() ), ) } @@ -275,8 +279,12 @@ class CredentialManagerUtils @Inject constructor( credentialIds: List, ) { credentialManager.signalCredentialState( - SignalAllAcceptedCredentialIdsRequest( - """{"rpId":"${dataStore.read(RP_ID_KEY)}","userId":"${dataStore.read(USER_ID_KEY)}","allAcceptedCredentialIds":["${credentialIds.joinToString(",")}"]}""", + request = SignalAllAcceptedCredentialIdsRequest( + requestJson = JSONObject().apply { + put("rpId", dataStore.read(RP_ID_KEY)) + put("userId", dataStore.read(USER_ID_KEY)) + put("allAcceptedCredentialIds", JSONArray(credentialIds)) + }.toString() ), ) } @@ -287,8 +295,13 @@ class CredentialManagerUtils @Inject constructor( newDisplayName: String, ) { credentialManager.signalCredentialState( - SignalCurrentUserDetailsRequest( - """{"rpId":"${dataStore.read(RP_ID_KEY)}","userId":"${dataStore.read(USER_ID_KEY)}", "name":"$newName","displayName":"$newDisplayName"}""", + request = SignalCurrentUserDetailsRequest( + requestJson = JSONObject().apply { + put("rpId", dataStore.read(RP_ID_KEY)) + put("userId", dataStore.read(USER_ID_KEY)) + put("name", newName) + put("displayName", newDisplayName) + }.toString() ), ) } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt index 138c8986..52625af6 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt @@ -180,7 +180,7 @@ private fun MainMenuButtonsList( ShrineButton( onClick = navigateToUpdateProfile, - buttonText = "Update Profile", + buttonText = stringResource(R.string.update_profile), ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt index 568f1a48..18d84f3c 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState <<<<<<< HEAD ======= -import androidx.compose.runtime.getValue >>>>>>> b12d692 (Add spotless fixes) import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -112,7 +111,7 @@ fun PasskeyManagementScreen( val onSignalBtnClicked = { val credentialIdsList = passkeysList - .filter { !it.isSelected } + .filter { it.isSelected } .map { it.id } if (credentialIdsList.isNotEmpty()) { viewModel.signalAccepted(credentialIdsList) @@ -191,7 +190,7 @@ fun PasskeyManagementScreen( ShrineButton( onClick = onSignal, - buttonText = "Delete selected credentials", + buttonText = stringResource(R.string.accept_selected_credentials), modifier = Modifier.fillMaxWidth(), ) } else { diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt index c7386f1d..3b35a98e 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt @@ -33,8 +33,16 @@ import com.authentication.shrine.ui.common.ShrineToolbar import com.authentication.shrine.ui.theme.ShrineTheme import com.authentication.shrine.ui.viewmodel.UpdateProfileViewModel +/** + * A stateful Composable screen that allows users to update their profile information, + * specifically their username and display name. + * + * @param onBackClicked Lambda to be invoked when the back button in the toolbar is clicked. + * @param viewModel An instance of [UpdateProfileViewModel] used to handle the business logic + */ @Composable fun UpdateProfileScreen( + onBackClicked: () -> Unit, viewModel: UpdateProfileViewModel, ) { var username by remember { mutableStateOf("") } @@ -43,24 +51,39 @@ fun UpdateProfileScreen( UpdateProfileScreen( username = username, onUsernameChanged = { username = it }, - email = email, - onEmailChanged = { email = it }, + displayName = email, + onDisplayNameChanged = { email = it }, onMetadataUpdate = viewModel::updateMetadata, + onBackClicked = onBackClicked, ) } +/** + * A stateless Composable screen that provides the UI for updating user profile information. + * + * @param username The current value of the username to be displayed in the text field. + * @param onUsernameChanged Lambda to update the username on change. + * @param displayName The current value of the display name to be displayed in the text field. + * @param onDisplayNameChanged Lambda to update the display name on change + * @param onMetadataUpdate Lambda function that is invoked when the update button is clicked. + * It receives the current username and display name strings as parameters, + * @param onBackClicked Lambda function to be invoked when the back button in the toolbar is clicked + */ @Composable fun UpdateProfileScreen( username: String, onUsernameChanged: (String) -> Unit, - email: String, - onEmailChanged: (String) -> Unit, + displayName: String, + onDisplayNameChanged: (String) -> Unit, onMetadataUpdate: (String, String) -> Unit, + onBackClicked: () -> Unit, ) { Column( modifier = Modifier.fillMaxSize(), ) { - ShrineToolbar(onBackClicked = {}) + ShrineToolbar( + onBackClicked = onBackClicked + ) ShrineTextField( value = username, @@ -70,29 +93,33 @@ fun UpdateProfileScreen( ) ShrineTextField( - value = email, - onValueChange = onEmailChanged, - hint = stringResource(R.string.email_address), + value = displayName, + onValueChange = onDisplayNameChanged, + hint = stringResource(R.string.display_name), modifier = Modifier.fillMaxWidth(), ) ShrineButton( - onClick = { onMetadataUpdate(username, email) }, + onClick = { onMetadataUpdate(username, displayName) }, buttonText = stringResource(R.string.update_user_info), ) } } +/** + * Preview Composable function of [UpdateProfileScreen] + */ @Preview(showBackground = true, showSystemUi = true) @Composable fun TestPreview() { ShrineTheme { UpdateProfileScreen( - "", - {}, - "", - {}, - { _, _ -> }, + username = "", + onUsernameChanged = { }, + displayName = "", + onDisplayNameChanged = { }, + onMetadataUpdate = { _, _ -> }, + onBackClicked = { } ) } } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt index f601e7df..6e154332 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/navigation/ShrineNavGraph.kt @@ -181,6 +181,7 @@ fun ShrineNavGraph( composable(route = ShrineAppDestinations.UpdateProfileRoute.name) { UpdateProfileScreen( + onBackClicked = { navController.popBackStack() }, viewModel = hiltViewModel(), ) } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt index 0adf6384..03f93d66 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/PasskeyManagementViewModel.kt @@ -275,6 +275,9 @@ class PasskeyManagementViewModel @Inject constructor( } } + /** + * Update list of items + */ fun updateItem(index: Int, passkeysList: List) { _uiState.update { it.copy( diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt index f525bf99..6d4b766f 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt @@ -18,32 +18,38 @@ package com.authentication.shrine.ui.viewmodel import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.authentication.shrine.CredentialManagerUtils import com.authentication.shrine.repository.AuthRepository.Companion.CRED_ID import com.authentication.shrine.repository.AuthRepository.Companion.RP_ID_KEY import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY import com.authentication.shrine.repository.AuthRepository.Companion.read 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 import kotlinx.coroutines.launch import javax.inject.Inject +/** + * ViewModel responsible for managing the state and business logic for the user profile + * update screen. + * + * @property credentialManagerUtils Utilities for interacting with the Credential Manager. + * @property dataStore The DataStore instance used for reading persisted user and credential identifiers. + */ @HiltViewModel class UpdateProfileViewModel @Inject constructor( private val credentialManagerUtils: CredentialManagerUtils, private val dataStore: DataStore, - private val coroutineScope: CoroutineScope, ) : ViewModel() { - private val _uiState = MutableStateFlow(TestState()) + private val _uiState = MutableStateFlow(UpdateProfileState()) val uiState = _uiState.asStateFlow() init { - coroutineScope.launch { + viewModelScope.launch { _uiState.update { - TestState( + UpdateProfileState( userId = dataStore.read(USER_ID_KEY) ?: "", rpId = dataStore.read(RP_ID_KEY) ?: "", credentialId = dataStore.read(CRED_ID) ?: "", @@ -52,17 +58,31 @@ class UpdateProfileViewModel @Inject constructor( } } + /** + * Signals an update to the user's metadata (name and display name) through the + * [CredentialManagerUtils]. + * + * @param newName The new name for the user. + * @param newDisplayName The new display name for the user. + */ fun updateMetadata( newName: String, newDisplayName: String, ) { - coroutineScope.launch { + viewModelScope.launch { credentialManagerUtils.signalUserDetails(newName, newDisplayName) } } } -data class TestState( +/** + * Represents the state of the user profile update screen. + * + * @property userId The unique identifier for the user + * @property rpId The identifier for the Relying Party + * @property credentialId The identifier for the credential that needs to be updated + */ +data class UpdateProfileState( val userId: String = "", val rpId: String = "", val credentialId: String = "", diff --git a/Shrine/app/src/main/res/values/strings.xml b/Shrine/app/src/main/res/values/strings.xml index 665ffe83..88bfc9b4 100644 --- a/Shrine/app/src/main/res/values/strings.xml +++ b/Shrine/app/src/main/res/values/strings.xml @@ -104,4 +104,6 @@ Invalid credentials. Please check your username and password. Update User Info Update Profile + Accept selected credentials + Display Name