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..5a3ad09 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,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,6 +57,7 @@ import javax.inject.Inject */ class CredentialManagerUtils @Inject constructor( private val credentialManager: CredentialManager, + private val dataStore: DataStore, ) { /** @@ -108,7 +118,7 @@ class CredentialManagerUtils @Inject constructor( .setServerClientId(SERVER_CLIENT_ID) .setFilterByAuthorizedAccounts(false) .build(), - ) + ), ) result = credentialManager.getCredential(context, credentialRequest) } catch (e: GetCredentialCancellationException) { @@ -249,6 +259,52 @@ class CredentialManagerUtils @Inject constructor( val clearRequest = ClearCredentialStateRequest(requestType = TYPE_CLEAR_RESTORE_CREDENTIAL) credentialManager.clearCredentialState(clearRequest) } + + @SuppressLint("RestrictedApi") + suspend fun signalUnknown( + credentialId: String, + ) { + credentialManager.signalCredentialState( + request = SignalUnknownCredentialRequest( + requestJson = JSONObject().apply { + put("rpId", dataStore.read(RP_ID_KEY)) + put("credentialId", credentialId) + }.toString() + ), + ) + } + + @SuppressLint("RestrictedApi") + suspend fun signalAcceptedIds( + credentialIds: List, + ) { + credentialManager.signalCredentialState( + request = SignalAllAcceptedCredentialIdsRequest( + requestJson = JSONObject().apply { + put("rpId", dataStore.read(RP_ID_KEY)) + put("userId", dataStore.read(USER_ID_KEY)) + put("allAcceptedCredentialIds", JSONArray(credentialIds)) + }.toString() + ), + ) + } + + @SuppressLint("RestrictedApi") + suspend fun signalUserDetails( + newName: String, + newDisplayName: String, + ) { + credentialManager.signalCredentialState( + 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() + ), + ) + } } 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 dfef9b3..406339c 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 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 e355d3f..eed1354 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 } @@ -421,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) @@ -545,6 +558,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() @@ -634,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 9937f2d..99dccbc 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/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 7cb7082..18d84f3 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 @@ -40,6 +42,9 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +<<<<<<< HEAD +======= +>>>>>>> b12d692 (Add spotless fixes) import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -94,7 +99,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 +124,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 +148,8 @@ fun PasskeyManagementScreen( uiState: PasskeyManagementUiState, passkeysList: List, aaguidData: Map>, + onItemClick: (Int) -> Unit, + onSignal: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = remember { SnackbarHostState() } @@ -161,6 +185,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( @@ -185,7 +216,7 @@ fun PasskeyManagementScreen( if (snackbarMessage != null) { LaunchedEffect(snackbarMessage) { snackbarHostState.showSnackbar( - message = snackbarMessage + message = snackbarMessage, ) } } @@ -202,6 +233,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 +254,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 +287,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 +304,12 @@ fun PasskeysDetailsRow( .build(), ) + Checkbox( + checked = isChecked, + onCheckedChange = {}, + modifier = Modifier.clickable(enabled = false, onClick = { }), + ) + Image( modifier = Modifier.size(48.dp), painter = painter, @@ -331,8 +373,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/SettingsScreen.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/SettingsScreen.kt index cff5484..a81f081 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 0000000..3b35a98 --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/UpdateProfileScreen.kt @@ -0,0 +1,125 @@ +/* + * 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 + +/** + * 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 email by remember { mutableStateOf("") } + + UpdateProfileScreen( + username = username, + onUsernameChanged = { username = 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, + displayName: String, + onDisplayNameChanged: (String) -> Unit, + onMetadataUpdate: (String, String) -> Unit, + onBackClicked: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + ShrineToolbar( + onBackClicked = onBackClicked + ) + + ShrineTextField( + value = username, + onValueChange = onUsernameChanged, + hint = stringResource(R.string.username), + modifier = Modifier.fillMaxWidth(), + ) + + ShrineTextField( + value = displayName, + onValueChange = onDisplayNameChanged, + hint = stringResource(R.string.display_name), + modifier = Modifier.fillMaxWidth(), + ) + + 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..f29298f 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,10 +28,21 @@ 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 +/** + * 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 +73,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 34838c5..88bfc9b 100644 --- a/Shrine/app/src/main/res/values/strings.xml +++ b/Shrine/app/src/main/res/values/strings.xml @@ -102,4 +102,8 @@ An unexpected 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 26b7a69..7f3aac9 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 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.