diff --git a/Shrine/app/build.gradle.kts b/Shrine/app/build.gradle.kts index cab144dd..3d974bcf 100644 --- a/Shrine/app/build.gradle.kts +++ b/Shrine/app/build.gradle.kts @@ -183,4 +183,8 @@ dependencies { implementation(libs.lifecycle.runtime.compose) implementation(libs.gms.location) implementation(libs.androidx.core.splashscreen) + + testImplementation(libs.androidx.core.testing) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.truth) } diff --git a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt index fd6e4624..2b3f298b 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt @@ -108,7 +108,7 @@ class CredentialManagerUtils @Inject constructor( .setServerClientId(SERVER_CLIENT_ID) .setFilterByAuthorizedAccounts(false) .build(), - ) + ), ) result = credentialManager.getCredential(context, credentialRequest) } catch (e: GetCredentialCancellationException) { diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt b/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt index dac86d1e..694fca8f 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt @@ -16,111 +16,10 @@ package com.authentication.shrine import android.app.Application -import android.content.Context -import android.os.Build -import androidx.credentials.CredentialManager -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStoreFile -import com.authentication.shrine.api.AddHeaderInterceptor -import com.authentication.shrine.api.AuthApiService -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn import dagger.hilt.android.HiltAndroidApp -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit -import javax.inject.Singleton /** * The main application class for the Shrine app. */ @HiltAndroidApp class ShrineApplication : Application() - -/** - * A Dagger Hilt module that provides dependencies for the application. - */ -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - - /** - * Creates and provides an OkHttpClient instance with interceptors and timeouts. - * - * @return The OkHttpClient instance. - */ - @Singleton - @Provides - fun provideOkHttpClient(): OkHttpClient { - val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} " + - "(Android ${Build.VERSION.RELEASE}; ${Build.MODEL}; ${Build.BRAND})" - return OkHttpClient.Builder() - .addInterceptor(AddHeaderInterceptor(userAgent)) - .addInterceptor( - HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - }, - ) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(40, TimeUnit.SECONDS) - .connectTimeout(40, TimeUnit.SECONDS) - .build() - } - - /** - * Provides a singleton instance of the CoroutineScope. - * - * @return The CoroutineScope instance. - */ - @Singleton - @Provides - fun provideAppCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob()) - - /** - * Provides a DataStore instance with the file name "auth". - * - * @param application The application context. - * @return The DataStore instance. - */ - @Singleton - @Provides - fun provideDataStore(application: Application): DataStore { - return PreferenceDataStoreFactory.create { - application.preferencesDataStoreFile("auth") - } - } - - @Singleton - @Provides - fun providesCredentialManager(@ApplicationContext context: Context): CredentialManager { - return CredentialManager.create(context) - } - - @Singleton - @Provides - fun providesCredentialManagerUtils( - credentialManager: CredentialManager, - ): CredentialManagerUtils { - return CredentialManagerUtils(credentialManager) - } - - @Singleton - @Provides - fun provideAuthApiService(okHttpClient: OkHttpClient): AuthApiService { - return Retrofit.Builder() - .baseUrl(BuildConfig.API_BASE_URL) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(AuthApiService::class.java) - } -} diff --git a/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt index dfef9b32..406339c7 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt @@ -165,7 +165,7 @@ interface AuthApiService { */ @POST("federation/options") suspend fun getFederationOptions( - @Body urls: FederationOptionsRequest + @Body urls: FederationOptionsRequest, ): Response /** diff --git a/Shrine/app/src/main/java/com/authentication/shrine/di/AppModule.kt b/Shrine/app/src/main/java/com/authentication/shrine/di/AppModule.kt new file mode 100644 index 00000000..4ffa4a31 --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/di/AppModule.kt @@ -0,0 +1,135 @@ +/* + * 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.di + +import android.app.Application +import android.content.Context +import android.os.Build +import androidx.credentials.CredentialManager +import androidx.datastore.core.DataStore +import androidx.datastore.dataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import com.authentication.shrine.BuildConfig +import com.authentication.shrine.CredentialManagerUtils +import com.authentication.shrine.api.AddHeaderInterceptor +import com.authentication.shrine.api.AuthApiService +import com.authentication.shrine.repository.AuthRepository +import com.authentication.shrine.repository.AuthenticationRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +/** + * A Dagger Hilt module that provides dependencies for the application. + */ +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + /** + * Creates and provides an OkHttpClient instance with interceptors and timeouts. + * + * @return The OkHttpClient instance. + */ + @Singleton + @Provides + fun provideOkHttpClient(): OkHttpClient { + val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} " + + "(Android ${Build.VERSION.RELEASE}; ${Build.MODEL}; ${Build.BRAND})" + return OkHttpClient.Builder() + .addInterceptor(AddHeaderInterceptor(userAgent)) + .addInterceptor( + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }, + ) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(40, TimeUnit.SECONDS) + .connectTimeout(40, TimeUnit.SECONDS) + .build() + } + + /** + * Provides a singleton instance of the CoroutineScope. + * + * @return The CoroutineScope instance. + */ + @Singleton + @Provides + fun provideAppCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob()) + + /** + * Provides a DataStore instance with the file name "auth". + * + * @param application The application context. + * @return The DataStore instance. + */ + @Singleton + @Provides + fun provideDataStore(application: Application): DataStore { + return PreferenceDataStoreFactory.create { + application.preferencesDataStoreFile("auth") + } + } + + @Singleton + @Provides + fun providesCredentialManager(@ApplicationContext context: Context): CredentialManager { + return CredentialManager.create(context) + } + + @Singleton + @Provides + fun providesCredentialManagerUtils( + credentialManager: CredentialManager, + ): CredentialManagerUtils { + return CredentialManagerUtils( + credentialManager = credentialManager, + ) + } + + @Singleton + @Provides + fun provideAuthApiService(okHttpClient: OkHttpClient): AuthApiService { + return Retrofit.Builder() + .baseUrl(BuildConfig.API_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(AuthApiService::class.java) + } + + @Singleton + @Provides + fun provideAuthRepository( + dataStore: DataStore, + authApiService: AuthApiService, + ): AuthenticationRepository { + return AuthRepository(dataStore, authApiService) + } +} diff --git a/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt index 16f2d6a3..254a17db 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/model/FederationOptionsRequest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.authentication.shrine.model /** @@ -5,5 +20,5 @@ package com.authentication.shrine.model * @param urls a list of urls to send for federated requests. */ data class FederationOptionsRequest( - val urls: List = listOf("https://accounts.google.com") -) \ No newline at end of file + val urls: List = listOf("https://accounts.google.com"), +) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt b/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt index 9ae7d1f6..ba38bc0c 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/model/SignInWithGoogleRequest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.authentication.shrine.model /** @@ -7,5 +22,5 @@ package com.authentication.shrine.model */ data class SignInWithGoogleRequest( val token: String, - val url: String = "https://accounts.google.com" -) \ No newline at end of file + val url: String = "https://accounts.google.com", +) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt index 1afe872d..281d9a3f 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 @@ -50,7 +50,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import org.json.JSONObject import javax.inject.Inject -import javax.inject.Singleton const val SERVER_CLIENT_ID = "493201854729-bposa1duevdn4nspp28cmn6anucu60pf.apps.googleusercontent.com" @@ -61,11 +60,10 @@ const val SERVER_CLIENT_ID = * @param authApiService The API service for interacting with the server. * @param dataStore The data store for storing user data. */ -@Singleton class AuthRepository @Inject constructor( private val dataStore: DataStore, private val authApiService: AuthApiService, -) { +) : AuthenticationRepository { // Companion object for constants and helper methods companion object { @@ -92,7 +90,7 @@ class AuthRepository @Inject constructor( * @param username The username to send. * @return True if the login was successful, false otherwise. */ - suspend fun registerUsername(username: String): Boolean { + override suspend fun registerUsername(username: String): Boolean { val response = authApiService.registerUsername(UsernameRequest(username)) if (response.isSuccessful) { dataStore.edit { prefs -> @@ -117,7 +115,7 @@ class AuthRepository @Inject constructor( * @param password The password to send. * @return True if the login was successful, false otherwise. */ - suspend fun login(username: String, password: String): Boolean { + override suspend fun login(username: String, password: String): Boolean { val response = authApiService.setUsername(UsernameRequest(username = username)) if (response.isSuccessful) { dataStore.edit { prefs -> @@ -182,7 +180,7 @@ class AuthRepository @Inject constructor( /** * Clears all the sign-in information. */ - suspend fun signOut() { + override suspend fun signOut() { dataStore.edit { prefs -> prefs.remove(USERNAME) prefs.remove(SESSION_ID) @@ -196,7 +194,7 @@ class AuthRepository @Inject constructor( * * @return The public key credential request options, or null if there was an error. */ - suspend fun registerPasskeyCreationRequest(): JSONObject? { + override suspend fun registerPasskeyCreationRequest(): JSONObject? { try { val sessionId = dataStore.read(SESSION_ID) if (!sessionId.isNullOrEmpty()) { @@ -230,7 +228,7 @@ class AuthRepository @Inject constructor( * @param credentialResponse The credential response. * @return True if the registration was successful, false otherwise. */ - suspend fun registerPasskeyCreationResponse( + override suspend fun registerPasskeyCreationResponse( credentialResponse: CreateCredentialResponse, ): Boolean { try { @@ -294,7 +292,7 @@ class AuthRepository @Inject constructor( * * @return The public key credential request options, or null if there was an error. */ - suspend fun signInWithPasskeyOrPasswordRequest(): JSONObject? { + override suspend fun signInWithPasskeyOrPasswordRequest(): JSONObject? { val response = authApiService.signInRequest() if (response.isSuccessful) { dataStore.edit { prefs -> @@ -318,7 +316,7 @@ class AuthRepository @Inject constructor( * @param credentialResponse The credential response. * @return True if the sign-in was successful, false otherwise. */ - suspend fun signInWithPasskeyOrPasswordResponse(credentialResponse: GetCredentialResponse): Boolean { + override suspend fun signInWithPasskeyOrPasswordResponse(credentialResponse: GetCredentialResponse): Boolean { val credential = credentialResponse.credential try { if (credential is PublicKeyCredential) { @@ -387,9 +385,9 @@ class AuthRepository @Inject constructor( * @param sessionId The session ID retrieved from the server via federation options request. * @param credentialResponse The credential retrieved from Credential Manager. */ - suspend fun signInWithFederatedTokenResponse( + override suspend fun signInWithFederatedTokenResponse( sessionId: String, - credentialResponse: GetCredentialResponse + credentialResponse: GetCredentialResponse, ): Boolean { val credential = credentialResponse.credential try { @@ -397,7 +395,7 @@ class AuthRepository @Inject constructor( return verifyIdToken( sessionId, GoogleIdTokenCredential - .createFrom(credential.data).idToken + .createFrom(credential.data).idToken, ) } else { Log.e(TAG, "Invalid federated token credential") @@ -413,7 +411,7 @@ class AuthRepository @Inject constructor( * * @return True if the user is signed in, false otherwise. */ - suspend fun isSignedInThroughPassword(): Boolean { + override suspend fun isSignedInThroughPassword(): Boolean { val sessionId = dataStore.read(SESSION_ID) return when { sessionId.isNullOrBlank() -> false @@ -426,7 +424,7 @@ class AuthRepository @Inject constructor( * * @return True if the user is signed in through passkeys, false otherwise. */ - suspend fun isSignedInThroughPasskeys(): Boolean { + override suspend fun isSignedInThroughPasskeys(): Boolean { val isSignedInThroughPasskeys = dataStore.read(IS_SIGNED_IN_THROUGH_PASSKEYS) isSignedInThroughPasskeys?.let { return it @@ -439,7 +437,7 @@ class AuthRepository @Inject constructor( * * @param flag True if the user is signed in through passkeys, false otherwise. */ - suspend fun setSignedInState(flag: Boolean) { + override suspend fun setSignedInState(flag: Boolean) { dataStore.edit { prefs -> prefs[IS_SIGNED_IN_THROUGH_PASSKEYS] = flag } @@ -450,7 +448,7 @@ class AuthRepository @Inject constructor( * * This is a suspend function that edits the data store to remove the session ID. */ - suspend fun clearSessionIdFromDataStore() { + override suspend fun clearSessionIdFromDataStore() { dataStore.edit { prefs -> prefs.remove(SESSION_ID) } @@ -463,7 +461,7 @@ class AuthRepository @Inject constructor( * * @return The stored username as a [String]. Returns an empty string if no username is found. */ - suspend fun getUsername(): String { + override suspend fun getUsername(): String { return dataStore.read(USERNAME).orEmpty() } @@ -472,7 +470,7 @@ class AuthRepository @Inject constructor( * * @return [PasskeysList] Object holding a list of Passkey details * */ - suspend fun getListOfPasskeys(): PasskeysList? { + override suspend fun getListOfPasskeys(): PasskeysList? { val sessionId = dataStore.read(SESSION_ID) if (!sessionId.isNullOrBlank()) { val apiResult = authApiService.getKeys( @@ -494,7 +492,7 @@ class AuthRepository @Inject constructor( * @param credentialId The ID of the credential to be deleted * @return True if the deletion was successful, false otherwise */ - suspend fun deletePasskey(credentialId: String): Boolean { + override suspend fun deletePasskey(credentialId: String): Boolean { val sessionId = dataStore.read(SESSION_ID) // Construct endpoint for deleting passkeys. try { @@ -515,7 +513,7 @@ class AuthRepository @Inject constructor( return false } - suspend fun deleteRestoreKeyFromServer(): Boolean { + override suspend fun deleteRestoreKeyFromServer(): Boolean { val sessionId = dataStore.read(SESSION_ID) val credentialId = dataStore.read(RESTORE_KEY_CREDENTIAL_ID) // Construct endpoint for deleting passkeys. @@ -531,7 +529,8 @@ class AuthRepository @Inject constructor( signOut() } } - }catch (e: Exception) { + } + catch (e: Exception) { Log.e(TAG, "Cannot call deleteRestoreKey", e) } return false @@ -544,7 +543,7 @@ class AuthRepository @Inject constructor( * on your server implementation. * @return The client ID stored as the session ID as a [String]. */ - suspend fun getFederationOptions(): String? { + override suspend fun getFederationOptions(): String? { val apiResult = authApiService.getFederationOptions(FederationOptionsRequest()) if (apiResult.isSuccessful) { return apiResult.getSessionId() @@ -559,10 +558,10 @@ class AuthRepository @Inject constructor( * is treated as a session ID for this server implementation. * @param token The ID token to be authorized. */ - suspend fun verifyIdToken(sessionId: String, token: String): Boolean { + private 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/repository/AuthenticationRepository.kt b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthenticationRepository.kt new file mode 100644 index 00000000..3cb26e4f --- /dev/null +++ b/Shrine/app/src/main/java/com/authentication/shrine/repository/AuthenticationRepository.kt @@ -0,0 +1,58 @@ +/* + * 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.repository + +import androidx.credentials.CreateCredentialResponse +import androidx.credentials.GetCredentialResponse +import com.authentication.shrine.model.PasskeysList +import org.json.JSONObject + +interface AuthenticationRepository { + + suspend fun registerUsername(username: String): Boolean + + suspend fun login(username: String, password: String): Boolean + + suspend fun signOut() + + suspend fun registerPasskeyCreationRequest(): JSONObject? + + suspend fun registerPasskeyCreationResponse(credentialResponse: CreateCredentialResponse): Boolean + + suspend fun signInWithPasskeyOrPasswordRequest(): JSONObject? + + suspend fun signInWithPasskeyOrPasswordResponse(credentialResponse: GetCredentialResponse): Boolean + + suspend fun isSignedInThroughPassword(): Boolean + + suspend fun isSignedInThroughPasskeys(): Boolean + + suspend fun setSignedInState(flag: Boolean) + + suspend fun clearSessionIdFromDataStore() + + suspend fun getUsername(): String + + suspend fun getListOfPasskeys(): PasskeysList? + + suspend fun deletePasskey(credentialId: String): Boolean + + suspend fun deleteRestoreKeyFromServer(): Boolean + + suspend fun signInWithFederatedTokenResponse(sessionId: String, credentialResponse: GetCredentialResponse): Boolean + + suspend fun getFederationOptions(): String? +} 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 15c7faa3..1e4d1442 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 @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold @@ -200,7 +199,8 @@ fun AuthenticationScreen( .height(dimensionResource(R.dimen.siwg_button_height)) .clickable( enabled = !uiState.isLoading, - onClick = onSignInWithSignInWithGoogleRequest) + onClick = onSignInWithSignInWithGoogleRequest, + ), ) } @@ -236,7 +236,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 a949e231..80267979 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 @@ -26,6 +26,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -59,6 +60,8 @@ fun MainMenuScreen( modifier: Modifier = Modifier, credentialManagerUtils: CredentialManagerUtils, ) { + val uiState = viewModel.uiState.collectAsState().value + val onSignOut = { viewModel.signOut( deleteRestoreKey = credentialManagerUtils::deleteRestoreKey, @@ -69,10 +72,13 @@ fun MainMenuScreen( onShrineButtonClicked = onShrineButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked, onHelpButtonClicked = onHelpButtonClicked, - navigateToLogin = navigateToLogin, onSignOut = onSignOut, modifier = modifier, ) + + if (!uiState.isSignedIn) { + navigateToLogin() + } } /** @@ -84,7 +90,6 @@ fun MainMenuScreen( * @param onShrineButtonClicked Callback to navigate to the Shrine app. * @param onSettingsButtonClicked Callback to navigate to the settings screen. * @param onHelpButtonClicked Callback to navigate to the help screen. - * @param navigateToLogin Callback to navigate to the login screen. * @param onSignOut Callback to sign out the user. */ @Composable @@ -92,7 +97,6 @@ fun MainMenuScreen( onShrineButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit, onHelpButtonClicked: () -> Unit, - navigateToLogin: () -> Unit, onSignOut: () -> Unit, modifier: Modifier = Modifier, ) { @@ -120,7 +124,6 @@ fun MainMenuScreen( onSettingsButtonClicked, onHelpButtonClicked, onSignOut, - navigateToLogin, ) } } @@ -133,7 +136,6 @@ fun MainMenuScreen( * @param onSettingsButtonClicked Callback invoked when the "Settings" button is clicked. * @param onHelpButtonClicked Callback invoked when the "Help" button is clicked. * @param onSignOut Callback invoked when the "Sign Out" button is clicked. - * @param navigateToLogin Callback invoked to navigate to the login screen after signing out. */ @Composable private fun MainMenuButtonsList( @@ -141,7 +143,6 @@ private fun MainMenuButtonsList( onSettingsButtonClicked: () -> Unit, onHelpButtonClicked: () -> Unit, onSignOut: () -> Unit, - navigateToLogin: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -165,10 +166,7 @@ private fun MainMenuButtonsList( ) ShrineButton( - onClick = { - onSignOut() - navigateToLogin() - }, + onClick = onSignOut, buttonText = stringResource(R.string.sign_out), isButtonDark = false, ) @@ -186,7 +184,6 @@ fun PasskeysSignedPreview() { onShrineButtonClicked = { }, onSettingsButtonClicked = { }, onHelpButtonClicked = { }, - navigateToLogin = { }, onSignOut = { }, ) } 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 7ff3c8db..3df029c1 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 @@ -92,7 +92,8 @@ fun PasskeyManagementScreen( requestResult = data, context = context, ) - }) + }, + ) } } @@ -183,7 +184,7 @@ fun PasskeyManagementScreen( if (snackbarMessage != null) { LaunchedEffect(snackbarMessage) { snackbarHostState.showSnackbar( - message = snackbarMessage + message = snackbarMessage, ) } } @@ -224,7 +225,7 @@ fun PasskeysListColumn( 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.onBackground, @@ -321,8 +322,8 @@ fun PasskeyManagementScreenPreview() { "passkey", "aaguid", 1L, - "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4" - ) + "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4", + ), ), aaguidData = mapOf(), ) 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 5758a4bd..4bea9ae9 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 @@ -169,7 +169,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/viewmodel/AuthenticationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt index 82982f9b..a3a61a31 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/AuthenticationViewModel.kt @@ -60,7 +60,7 @@ class AuthenticationViewModel @Inject constructor( * Requests a sign-in challenge from the server. * * @param onSuccess Lambda that handles actions on successful passkey sign-in - * @param getPasskey Lambda that calls CredManUtil's getPasskey method with Activity reference + * @param getCredential Lambda that calls CredManUtil's getPasskey method with Activity reference */ fun signInWithPasskeyOrPasswordRequest( onSuccess: (Boolean) -> Unit, diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/HomeViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/HomeViewModel.kt index db05f51d..42b77807 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/HomeViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/HomeViewModel.kt @@ -17,8 +17,11 @@ package com.authentication.shrine.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.authentication.shrine.repository.AuthRepository +import com.authentication.shrine.repository.AuthenticationRepository 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 @@ -29,9 +32,12 @@ import javax.inject.Inject */ @HiltViewModel class HomeViewModel @Inject constructor( - private val repository: AuthRepository, + private val repository: AuthenticationRepository, ) : ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState = _uiState.asStateFlow() + /** * Signs out the user. * @@ -46,5 +52,12 @@ class HomeViewModel @Inject constructor( repository.deleteRestoreKeyFromServer() repository.signOut() } + _uiState.update { + it.copy(isSignedIn = false) + } } } + +data class HomeUiState( + val isSignedIn: Boolean = true, +) 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 913e43ab..cd7dbe2a 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 @@ -47,7 +47,7 @@ import javax.inject.Inject @HiltViewModel class PasskeyManagementViewModel @Inject constructor( private val authRepository: AuthRepository, - private val application: Application + private val application: Application, ) : ViewModel() { private val _uiState = MutableStateFlow(PasskeyManagementUiState()) val uiState = _uiState.asStateFlow() @@ -68,7 +68,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) { @@ -152,7 +152,7 @@ class PasskeyManagementViewModel @Inject constructor( it.copy( isLoading = false, passkeysList = filteredPasskeysList, - messageResourceId = R.string.passkey_created + messageResourceId = R.string.passkey_created, ) } } else { @@ -167,7 +167,7 @@ class PasskeyManagementViewModel @Inject constructor( _uiState.update { it.copy( isLoading = false, - messageResourceId = R.string.some_error_occurred_please_check_logs + messageResourceId = R.string.some_error_occurred_please_check_logs, ) } } @@ -175,7 +175,7 @@ class PasskeyManagementViewModel @Inject constructor( _uiState.update { it.copy( isLoading = false, - errorMessage = createPasskeyResponse.errorMessage + errorMessage = createPasskeyResponse.errorMessage, ) } authRepository.setSignedInState(false) @@ -184,7 +184,7 @@ class PasskeyManagementViewModel @Inject constructor( _uiState.update { it.copy( isLoading = false, - messageResourceId = R.string.oops_an_internal_server_error_occurred + messageResourceId = R.string.oops_an_internal_server_error_occurred, ) } } @@ -192,7 +192,7 @@ class PasskeyManagementViewModel @Inject constructor( _uiState.update { it.copy( isLoading = false, - errorMessage = e.message + errorMessage = e.message, ) } authRepository.setSignedInState(false) @@ -224,7 +224,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 { @@ -271,4 +271,4 @@ data class PasskeyManagementUiState( val passkeysList: List = listOf(), @StringRes val messageResourceId: Int = R.string.empty_string, val errorMessage: String? = null, -) \ No newline at end of file +) diff --git a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt index 70009706..90db3cb2 100644 --- a/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt +++ b/Shrine/app/src/main/java/com/authentication/shrine/ui/viewmodel/RegistrationViewModel.kt @@ -79,7 +79,7 @@ class RegistrationViewModel @Inject constructor( ) } } - } catch(e: Exception) { + } catch (e: Exception) { _uiState.update { RegisterUiState(errorMessage = e.message) } @@ -119,14 +119,14 @@ class RegistrationViewModel @Inject constructor( _uiState.update { RegisterUiState( isSuccess = true, - messageResourceId = R.string.passkey_created_try_signin_with_passkeys + messageResourceId = R.string.passkey_created_try_signin_with_passkeys, ) } } else { _uiState.update { RegisterUiState( isSuccess = false, - messageResourceId = R.string.some_error_occurred_please_check_logs + messageResourceId = R.string.some_error_occurred_please_check_logs, ) } } @@ -143,7 +143,7 @@ class RegistrationViewModel @Inject constructor( } onSuccess(false) } - } catch(e: Exception) { + } catch (e: Exception) { _uiState.update { RegisterUiState(errorMessage = e.message) } @@ -192,7 +192,7 @@ class RegistrationViewModel @Inject constructor( } } repository.setSignedInState(false) - } catch(e: Exception) { + } catch (e: Exception) { _uiState.update { RegisterUiState(errorMessage = e.message) } diff --git a/Shrine/app/src/test/java/com/authentication/shrine/repository/FakeAuthRepository.kt b/Shrine/app/src/test/java/com/authentication/shrine/repository/FakeAuthRepository.kt new file mode 100644 index 00000000..53b27e78 --- /dev/null +++ b/Shrine/app/src/test/java/com/authentication/shrine/repository/FakeAuthRepository.kt @@ -0,0 +1,94 @@ +/* + * 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.repository + +import androidx.credentials.CreateCredentialResponse +import androidx.credentials.GetCredentialResponse +import com.authentication.shrine.model.PasskeysList +import org.json.JSONObject + +class FakeAuthRepository : AuthenticationRepository { + override suspend fun registerUsername(username: String): Boolean { + TODO("Not yet implemented") + } + + override suspend fun login(username: String, password: String): Boolean { + TODO("Not yet implemented") + } + + override suspend fun signOut() { + TODO("Not yet implemented") + } + + override suspend fun registerPasskeyCreationRequest(): JSONObject? { + TODO("Not yet implemented") + } + + override suspend fun registerPasskeyCreationResponse(credentialResponse: CreateCredentialResponse): Boolean { + TODO("Not yet implemented") + } + + override suspend fun signInWithPasskeyOrPasswordRequest(): JSONObject? { + TODO("Not yet implemented") + } + + override suspend fun signInWithPasskeyOrPasswordResponse(credentialResponse: GetCredentialResponse): Boolean { + TODO("Not yet implemented") + } + + override suspend fun isSignedInThroughPassword(): Boolean { + TODO("Not yet implemented") + } + + override suspend fun isSignedInThroughPasskeys(): Boolean { + TODO("Not yet implemented") + } + + override suspend fun setSignedInState(flag: Boolean) { + TODO("Not yet implemented") + } + + override suspend fun clearSessionIdFromDataStore() { + TODO("Not yet implemented") + } + + override suspend fun getUsername(): String { + TODO("Not yet implemented") + } + + override suspend fun getListOfPasskeys(): PasskeysList? { + TODO("Not yet implemented") + } + + override suspend fun deletePasskey(credentialId: String): Boolean { + TODO("Not yet implemented") + } + + override suspend fun deleteRestoreKeyFromServer(): Boolean { + TODO("Not yet implemented") + } + + override suspend fun signInWithFederatedTokenResponse( + sessionId: String, + credentialResponse: GetCredentialResponse, + ): Boolean { + TODO("Not yet implemented") + } + + override suspend fun getFederationOptions(): String? { + TODO("Not yet implemented") + } +} diff --git a/Shrine/app/src/test/java/com/authentication/shrine/ui/viewmodel/HomeViewModelTest.kt b/Shrine/app/src/test/java/com/authentication/shrine/ui/viewmodel/HomeViewModelTest.kt new file mode 100644 index 00000000..538bc138 --- /dev/null +++ b/Shrine/app/src/test/java/com/authentication/shrine/ui/viewmodel/HomeViewModelTest.kt @@ -0,0 +1,53 @@ +/* + * 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 com.authentication.shrine.repository.AuthenticationRepository +import com.authentication.shrine.repository.FakeAuthRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HomeViewModelTest { + private lateinit var homeViewModel: HomeViewModel + private val fakeAuthRepository: AuthenticationRepository = FakeAuthRepository() + + @Before + fun setup() { + homeViewModel = HomeViewModel(fakeAuthRepository) + Dispatchers.setMain(StandardTestDispatcher()) + } + + @Test + fun `log the user out when signOut is called`() { + homeViewModel.signOut { } + + val state = homeViewModel.uiState.value + assertThat(state.isSignedIn).isFalse() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } +} diff --git a/Shrine/gradle/libs.versions.toml b/Shrine/gradle/libs.versions.toml index 4d8085e5..c4f20efa 100644 --- a/Shrine/gradle/libs.versions.toml +++ b/Shrine/gradle/libs.versions.toml @@ -1,11 +1,12 @@ [versions] activity = "1.9.0" androidxAppcompat = "1.7.0" -androidPlugin = "8.11.1" +androidPlugin = "8.8.2" browser = "1.8.0" coil = "2.7.0" coilSvg = "2.6.0" coreSplashscreen = "1.0.1" +coreTesting = "2.2.0" credentials = "1.5.0" datastorePrefs = "1.1.1" googleFonts = "1.6.8" @@ -18,6 +19,7 @@ horologist = "0.6.23" kotlin = "1.9.0" kotlinCoroutines = "1.0" kotlinxCoroutines = "1.9.0" +kotlinxCoroutinesTest = "1.5.1" kotlinxSerialization = "1.7.3" ksp = "1.9.0-1.0.13" ktlint = "0.50.0" @@ -32,6 +34,7 @@ playServicesWearable = "19.0.0" retrofit = "2.9.0" spotless = "6.21.0" sysUiController = "0.28.0" +truth = "1.1.3" wearComposeMaterial3 = "1.5.0-beta04" wearRemoteInteractions = "1.1.0" @@ -44,14 +47,13 @@ kotlinBom = "1.8.0" espressoCore = "3.5.1" extJUnit = "1.1.5" jUnit = "4.13.2" -composeMaterial = "1.2.1" -wearToolingPreview = "1.0.0" foundation = "1.8.3" [libraries] activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } +androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompat" } browser = { group = "androidx.browser", name = "browser", version.ref = "browser" } coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } @@ -66,6 +68,7 @@ google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts", v google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleid" } google-services-plugin = { group = "com.google.gms", name = "google-services", version.ref = "googleServicesPlugin" } hilt-android-plugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "playServicesAuth" } play-services-fido = { module = "com.google.android.gms:play-services-fido", version.ref = "playServicesFido" } @@ -112,6 +115,7 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHtt # For Wear Module horologist-auth-ui = { module = "com.google.android.horologist:horologist-auth-ui", version.ref = "horologist" } horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeMaterial3" } wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "wearComposeMaterial3" } @@ -132,8 +136,6 @@ compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "extJUnit" } junit = { group = "junit", name = "junit", version.ref = "jUnit"} -androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } -androidx-wear-tooling-preview = { group = "androidx.wear", name = "wear-tooling-preview", version.ref = "wearToolingPreview" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } @@ -144,4 +146,3 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } -kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/Shrine/wear/build.gradle.kts b/Shrine/wear/build.gradle.kts index e574af91..a3f6026e 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) 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 14142d00..a79d9af9 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 @@ -99,7 +99,7 @@ class AuthenticationServer { 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(AddHeaderInterceptor(userAgent)) .addInterceptor( @@ -364,8 +364,10 @@ class AuthenticationServer { } } - return when (val authorizationResult = - authorizeFederatedTokenWithServer(federatedToken, federatedSessionId)) { + return when ( + val authorizationResult = + authorizeFederatedTokenWithServer(federatedToken, federatedSessionId) + ) { is ApiResult.Success -> { this.sessionId = authorizationResult.sessionId return true @@ -390,7 +392,8 @@ class AuthenticationServer { "POST", createJSONRequestBody { name("urls").beginArray().value("https://accounts.google.com").endArray() - }).build(), + }, + ).build(), ).await() return httpResponse.result(errorMessage = "Error creating federation options") {} @@ -407,10 +410,12 @@ class AuthenticationServer { * A [Unit] type for success implies no specific data is returned on successful authorization. */ private suspend fun authorizeFederatedTokenWithServer( - token: String, sessionId: String + token: String, + sessionId: String, ): ApiResult { val requestHeaders = okhttp3.Headers.Builder().add( - "Cookie", "$SESSION_ID_KEY$sessionId" + "Cookie", + "$SESSION_ID_KEY$sessionId", ).build() val httpResponse = httpClient.newCall( diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacyLoginScreen.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacyLoginScreen.kt index 66f6f319..e9cc8385 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacyLoginScreen.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacyLoginScreen.kt @@ -34,7 +34,7 @@ import androidx.wear.compose.material3.Text import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import com.authentication.shrinewear.Graph import com.authentication.shrinewear.R -//import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding +// import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding /** * Composable for the header of the legacy login options list. diff --git a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacySignInWithGoogleEventListener.kt b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacySignInWithGoogleEventListener.kt index c16d3792..6fba7547 100644 --- a/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/LegacySignInWithGoogleEventListener.kt +++ b/Shrine/wear/src/main/java/com/authentication/shrinewear/ui/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.