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 fd6e462..d368874 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt @@ -17,6 +17,7 @@ package com.authentication.shrine import android.annotation.SuppressLint import android.content.Context +import android.util.Log import androidx.credentials.ClearCredentialStateRequest import androidx.credentials.ClearCredentialStateRequest.Companion.TYPE_CLEAR_RESTORE_CREDENTIAL import androidx.credentials.CreateCredentialRequest @@ -33,11 +34,20 @@ 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 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.JSONArray import org.json.JSONObject import javax.inject.Inject @@ -48,8 +58,20 @@ import javax.inject.Inject */ class CredentialManagerUtils @Inject constructor( private val credentialManager: CredentialManager, + private val dataStore: DataStore, ) { + private val TAG = "CredentialManagerUtils" + + private object JSON_KEYS { + const val RP_ID = "rpId" + const val CREDENTIAL_ID = "credentialId" + const val USER_ID = "userId" + const val ALL_ACCEPTED_CREDENTIAL_IDS = "allAcceptedCredentialIds" + const val NAME = "name" + const val DISPLAY_NAME = "displayName" + } + /** * Retrieves a passkey or password credential from the credential manager. * @@ -108,7 +130,7 @@ class CredentialManagerUtils @Inject constructor( .setServerClientId(SERVER_CLIENT_ID) .setFilterByAuthorizedAccounts(false) .build(), - ) + ), ) result = credentialManager.getCredential(context, credentialRequest) } catch (e: GetCredentialCancellationException) { @@ -249,6 +271,73 @@ class CredentialManagerUtils @Inject constructor( val clearRequest = ClearCredentialStateRequest(requestType = TYPE_CLEAR_RESTORE_CREDENTIAL) credentialManager.clearCredentialState(clearRequest) } + + @SuppressLint("RestrictedApi") + suspend fun signalUnknown( + credentialId: String, + ) { + dataStore.read(RP_ID_KEY)?.let { rpId -> + credentialManager.signalCredentialState( + request = SignalUnknownCredentialRequest( + requestJson = JSONObject().apply { + put(JSON_KEYS.RP_ID, rpId) + put(JSON_KEYS.CREDENTIAL_ID, credentialId) + }.toString() + ), + ) + } ?: Log.e(TAG, "RP ID not present") + } + + @SuppressLint("RestrictedApi") + suspend fun signalAcceptedIds( + credentialIds: List, + ) { + fetchDataAndPerformAction { rpId, userId -> + credentialManager.signalCredentialState( + request = SignalAllAcceptedCredentialIdsRequest( + requestJson = JSONObject().apply { + put(JSON_KEYS.RP_ID, rpId) + put(JSON_KEYS.USER_ID, userId) + put(JSON_KEYS.ALL_ACCEPTED_CREDENTIAL_IDS, JSONArray(credentialIds)) + }.toString() + ), + ) + } + } + + @SuppressLint("RestrictedApi") + suspend fun signalUserDetails( + newName: String, + newDisplayName: String, + ) { + fetchDataAndPerformAction { rpId, userId -> + credentialManager.signalCredentialState( + request = SignalCurrentUserDetailsRequest( + requestJson = JSONObject().apply { + put(JSON_KEYS.RP_ID, rpId) + put(JSON_KEYS.USER_ID, userId) + put(JSON_KEYS.NAME, newName) + put(JSON_KEYS.DISPLAY_NAME, newDisplayName) + }.toString() + ), + ) + } + } + + suspend fun fetchDataAndPerformAction( + credentialManagerAction: suspend (rpId: String, userId: String) -> Unit + ) { + val rpId = dataStore.read(RP_ID_KEY) + val userId = dataStore.read(USER_ID_KEY) + + if (rpId.isNullOrBlank()) { + Log.e(TAG, "RP ID not present") + } else if (userId.isNullOrBlank()) { + Log.e(TAG, "User ID not present") + } else { + credentialManagerAction(rpId, userId) + } + } } 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 dac86d1..1a751f2 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/api/AuthApiService.kt b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt index e2d02dd..7de9642 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 @@ -166,7 +166,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 16f2d6a..254a17d 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/PasskeysList.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt index a2656cf..4e44818 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/model/SignInWithGoogleRequest.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt index 9ae7d1f..ba38bc0 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 de202fc..81b475f 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 @@ -81,6 +81,9 @@ class AuthRepository @Inject constructor( val DISPLAYNAME = stringPreferencesKey("displayname") 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 @@ -163,7 +166,6 @@ class AuthRepository @Inject constructor( } } - /** * Signs in with a password. * @@ -231,6 +233,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 } @@ -304,6 +307,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 } @@ -338,6 +342,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 } @@ -401,6 +406,7 @@ class AuthRepository @Inject constructor( ) return if (apiResult.isSuccessful) { dataStore.edit { prefs -> + prefs[CRED_ID] = credentialId apiResult.getSessionId()?.also { prefs[SESSION_ID] = it } @@ -453,7 +459,7 @@ class AuthRepository @Inject constructor( val isSuccess = verifyIdToken( sessionId, GoogleIdTokenCredential - .createFrom(credential.data).idToken + .createFrom(credential.data).idToken, ) if (isSuccess) { AuthResult.Success(Unit) @@ -581,6 +587,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() @@ -673,7 +682,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 e8bc641..927044b 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 @@ -69,6 +69,7 @@ fun AuthenticationScreen( credentialManagerUtils: CredentialManagerUtils, ) { val uiState = viewModel.uiState.collectAsState().value + var isFirstCheckForRestoreKey by remember { mutableStateOf(true) } // Passing in the lambda / context to the VM val context = LocalContext.current @@ -113,6 +114,18 @@ fun AuthenticationScreen( ) } + if (isFirstCheckForRestoreKey) { + isFirstCheckForRestoreKey = false + viewModel.checkForStoredRestoreKey( + getRestoreKey = { requestResult -> + credentialManagerUtils.getRestoreKey(requestResult, context) + }, + onSuccess = { + navigateToHome(true) + }, + ) + } + AuthenticationScreen( onSignInWithPasskeyOrPasswordRequest = onSignInWithPasskeyOrPasswordRequest, onSignInWithSignInWithGoogleRequest = onSignInWithSignInWithGoogleRequest, @@ -185,7 +198,8 @@ fun AuthenticationScreen( .height(dimensionResource(R.dimen.siwg_button_height)) .clickable( enabled = !uiState.isLoading, - onClick = onSignInWithSignInWithGoogleRequest) + onClick = onSignInWithSignInWithGoogleRequest + ) ) } } @@ -201,6 +215,7 @@ fun AuthenticationScreen( !uiState.isSignInWithPasskeysSuccess && stringResource(uiState.passkeyResponseMessageResourceId).isNotBlank() -> { stringResource(uiState.passkeyResponseMessageResourceId) } + else -> null } LaunchedEffect(uiState) { 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 821cac4..52625af 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 = stringResource(R.string.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 924bc6e..c9c205d 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 = stringResource(R.string.accept_selected_credentials), + modifier = Modifier.fillMaxWidth(), ) } else { ShrineButton( @@ -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,16 @@ fun PasskeysListColumn( iconSvgString = aaguidData[item.aaguid]?.get("icon_light"), credentialProviderName = item.name, passkeyCreationDate = item.registeredAt.toReadableDate(), + isChecked = item.isSelected, + onCheckboxClick = { onItemClick(index) }, + 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 +285,12 @@ fun PasskeysDetailsRow( iconSvgString: String?, credentialProviderName: String, passkeyCreationDate: String, + isChecked: Boolean, + onCheckboxClick: () -> Unit, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = dimensionResource(R.dimen.padding_small)), verticalAlignment = Alignment.CenterVertically, @@ -268,6 +303,11 @@ fun PasskeysDetailsRow( .build(), ) + Checkbox( + checked = isChecked, + onCheckedChange = { onCheckboxClick() }, + ) + Image( modifier = Modifier.size(48.dp), painter = painter, @@ -332,6 +372,8 @@ fun PasskeyManagementScreenPreview() { ) ), aaguidData = mapOf(), + onItemClick = { _ -> }, + onSignal = { }, ) } } 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 0000000..70715b0 --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt @@ -0,0 +1,124 @@ +/* + * 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.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 + +/** + * 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("") } + var displayName by remember { mutableStateOf("") } + + UpdateProfileScreen( + username = username, + onUsernameChanged = { username = it }, + displayName = displayName, + onDisplayNameChanged = { displayName = 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, + displayName: String, + onDisplayNameChanged: (String) -> Unit, + onMetadataUpdate: (String, String) -> Unit, + onBackClicked: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + ShrineToolbar( + onBackClicked = onBackClicked + ) + + ShrineTextField( + title = stringResource(R.string.username), + text = username, + enabled = true, + onValueChanged = onUsernameChanged + ) + + ShrineTextField( + title = stringResource(R.string.display_name), + text = displayName, + enabled = true, + onValueChanged = onDisplayNameChanged, + ) + + ShrineButton( + 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/common/ShrineTextField.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/common/ShrineTextField.kt index 65c8a8c..2526290 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,10 +32,18 @@ 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 title The current value of the TextField. + * @param text The callback to be invoked when the TextField value changes. + */ @Composable fun ShrineTextField( title: String, text: String = "", + enabled: Boolean = false, + onValueChanged: (String) -> Unit = {} ) { Column( modifier = Modifier @@ -50,10 +58,9 @@ fun ShrineTextField( OutlinedTextField( value = text, - onValueChange = { }, - modifier = Modifier - .fillMaxWidth(), - enabled = false, + onValueChange = onValueChanged, + modifier = Modifier.fillMaxWidth(), + enabled = enabled, shape = RoundedCornerShape(dimensionResource(R.dimen.size_standard)), colors = OutlinedTextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface @@ -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 66369fc..28a5c2c 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 88b65c9..6e15433 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,12 @@ fun ShrineNavGraph( composable(route = ShrineAppDestinations.ShrineApp.name) { ShrineAppScreen() } + + 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 11e81e8..03f93d6 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,29 @@ class PasskeyManagementViewModel @Inject constructor( } } } + + fun signalAccepted(credentialsIds: List) { + viewModelScope.launch { + credentialManagerUtils.signalAcceptedIds(credentialsIds) + } + } + + /** + * Update list of items + */ + 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 0000000..6d4b766 --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/UpdateProfileViewModel.kt @@ -0,0 +1,89 @@ +/* + * 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 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.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, +) : ViewModel() { + private val _uiState = MutableStateFlow(UpdateProfileState()) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.launch { + _uiState.update { + UpdateProfileState( + userId = dataStore.read(USER_ID_KEY) ?: "", + rpId = dataStore.read(RP_ID_KEY) ?: "", + credentialId = dataStore.read(CRED_ID) ?: "", + ) + } + } + } + + /** + * 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, + ) { + viewModelScope.launch { + credentialManagerUtils.signalUserDetails(newName, newDisplayName) + } + } +} + +/** + * 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 26e1ee6..298ca57 100644 --- a/Shrine/app/src/main/res/values/strings.xml +++ b/Shrine/app/src/main/res/values/strings.xml @@ -103,4 +103,8 @@ A server error occurred. An unknown error occurred. Invalid credentials. Please check your username and password. + Update User Info + Update Profile + Accept selected credentials + Display Name diff --git a/Shrine/gradle/libs.versions.toml b/Shrine/gradle/libs.versions.toml index 7af558a..6e7d061 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-beta02" 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 b97873c..77b36b2 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/Graph.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/Graph.kt index cda6fb9..6a74c43 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 3c5eed0..61d2000 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 bc20f8a..e2efbba 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 f0223f6..f4adf0a 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 98d36d5..61ddd4a 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 3b1b27c..41a4e02 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 03b7337..2a75cbf 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 d6c7071..c143bc2 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 8a740c0..eb416fe 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 5e6811d..3040719 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) { 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 585faed..43390de 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.