diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 37efe54b..b03c2acf 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -20,7 +20,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") buildConfigField("String", "APP_VERSION", "\"${libs.versions.versionName.get()}\"") - buildConfigField("String", "O_AUTH_WEB_CLIENT_ID", getApiKey("o_auth_web_client_id")) } buildTypes { @@ -76,9 +75,6 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.config.ktx) implementation(libs.firebase.analytics.ktx) - implementation(libs.credentials) - implementation(libs.credentials.auth) - implementation(libs.google.identity) debugImplementation(libs.bundles.flipper) releaseImplementation(libs.flipper.noop) diff --git a/data/src/main/java/com/nextroom/nextroom/data/di/FirebaseModule.kt b/data/src/main/java/com/nextroom/nextroom/data/di/FirebaseModule.kt index 8da9d4e0..9c368283 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/di/FirebaseModule.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/di/FirebaseModule.kt @@ -1,13 +1,9 @@ package com.nextroom.nextroom.data.di import android.content.Context -import androidx.credentials.CredentialManager -import androidx.credentials.GetCredentialRequest -import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.remoteConfigSettings -import com.nextroom.nextroom.data.BuildConfig import com.nextroom.nextroom.data.util.UserEventLoggerImpl import com.nextroom.nextroom.domain.util.UserEventLogger import dagger.Module @@ -45,23 +41,4 @@ object FirebaseModule { fun provideUserEventLogger(firebaseAnalytics: FirebaseAnalytics): UserEventLogger { return UserEventLoggerImpl(firebaseAnalytics) } - - @Singleton - @Provides - fun provideGetCredentialRequest(): GetCredentialRequest { - val googleIdOption = GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(false) - .setServerClientId(BuildConfig.O_AUTH_WEB_CLIENT_ID) - .build() - - return GetCredentialRequest.Builder() - .addCredentialOption(googleIdOption) - .build() - } - - @Singleton - @Provides - fun provideCredentialManager(@ApplicationContext context: Context): CredentialManager { - return CredentialManager.create(context) - } } diff --git a/data/src/main/java/com/nextroom/nextroom/data/di/RepositoryModule.kt b/data/src/main/java/com/nextroom/nextroom/data/di/RepositoryModule.kt index 42c8fa62..56433010 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/di/RepositoryModule.kt @@ -1,8 +1,6 @@ package com.nextroom.nextroom.data.di import android.content.Context -import androidx.credentials.CredentialManager -import androidx.credentials.GetCredentialRequest import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.nextroom.nextroom.data.datasource.AuthDataSource import com.nextroom.nextroom.data.datasource.BillingDataSource @@ -122,9 +120,6 @@ object RepositoryModule { settingDataSource: SettingDataSource, tokenDataSource: TokenDataSource, subscriptionDataSource: SubscriptionDataSource, - getCredentialRequest: GetCredentialRequest, - credentialManager: CredentialManager, - @ApplicationContext context: Context, apiService: ApiService, ): AdminRepository = AdminRepositoryImpl( authDataSource = authDataSource, @@ -132,9 +127,6 @@ object RepositoryModule { settingDataSource = settingDataSource, tokenDataSource = tokenDataSource, subscriptionDataSource = subscriptionDataSource, - getCredentialRequest = getCredentialRequest, - credentialManager = credentialManager, - context = context, apiService = apiService, ) diff --git a/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt b/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt index 86d26e5a..860b5457 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt @@ -1,12 +1,5 @@ package com.nextroom.nextroom.data.repository -import android.content.Context -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.nextroom.nextroom.data.datasource.AuthDataSource import com.nextroom.nextroom.data.datasource.SettingDataSource import com.nextroom.nextroom.data.datasource.SubscriptionDataSource @@ -16,7 +9,6 @@ import com.nextroom.nextroom.data.network.ApiService import com.nextroom.nextroom.data.network.response.AdditionalUserInfoRequestDto import com.nextroom.nextroom.data.network.response.GoogleLoginRequestDto import com.nextroom.nextroom.domain.model.AdditionalUserInfoResponse -import com.nextroom.nextroom.domain.model.GoogleAuthResponse import com.nextroom.nextroom.domain.model.GoogleLoginResponse import com.nextroom.nextroom.domain.model.LoginInfo import com.nextroom.nextroom.domain.model.Mypage @@ -36,9 +28,6 @@ class AdminRepositoryImpl @Inject constructor( private val settingDataSource: SettingDataSource, private val tokenDataSource: TokenDataSource, private val subscriptionDataSource: SubscriptionDataSource, - private val getCredentialRequest: GetCredentialRequest, - private val credentialManager: CredentialManager, - private val context: Context, private val apiService: ApiService, ) : AdminRepository { @@ -64,39 +53,6 @@ class AdminRepositoryImpl @Inject constructor( } } - override suspend fun requestGoogleAuth(): Result { - val result = credentialManager.getCredential( - request = getCredentialRequest, - context = context, - ) - return handleSignIn(result) - } - - private fun handleSignIn(result: GetCredentialResponse): Result.Success { - return when (val credential = result.credential) { - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) - Result.Success( - GoogleAuthResponse( - idToken = googleIdTokenCredential.idToken, - email = googleIdTokenCredential.id, - name = googleIdTokenCredential.displayName, - ) - ) - } catch (e: GoogleIdTokenParsingException) { - throw Exception("handleSignIn received an invalid google id token response", e) - } - } else { - throw Exception("unexpected type of credential") - } - } - - else -> throw Exception("unexpected type of credential") - } - } - override suspend fun logout() { authDataSource.logout() } diff --git a/domain/src/main/java/com/nextroom/nextroom/domain/model/GoogleAuthResponse.kt b/domain/src/main/java/com/nextroom/nextroom/domain/model/GoogleAuthResponse.kt deleted file mode 100644 index 1c4e6bf3..00000000 --- a/domain/src/main/java/com/nextroom/nextroom/domain/model/GoogleAuthResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.nextroom.nextroom.domain.model - -data class GoogleAuthResponse( - val idToken: String, - val email: String, - val name: String?, -) \ No newline at end of file diff --git a/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt b/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt index 29577ef8..f9198850 100644 --- a/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt +++ b/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt @@ -1,7 +1,6 @@ package com.nextroom.nextroom.domain.repository import com.nextroom.nextroom.domain.model.AdditionalUserInfoResponse -import com.nextroom.nextroom.domain.model.GoogleAuthResponse import com.nextroom.nextroom.domain.model.GoogleLoginResponse import com.nextroom.nextroom.domain.model.LoginInfo import com.nextroom.nextroom.domain.model.Mypage @@ -22,7 +21,6 @@ interface AdminRepository { * @return shopName * */ suspend fun login(adminCode: String, password: String, emailSaveChecked: Boolean): Result - suspend fun requestGoogleAuth(): Result suspend fun logout() suspend fun resign(): Result suspend fun verifyAdminCode(code: String): Boolean diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index a62c1673..e9256cf0 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.android.library) @@ -17,6 +19,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") + buildConfigField("String", "O_AUTH_WEB_CLIENT_ID", getApiKey("o_auth_web_client_id")) } buildTypes { @@ -91,3 +94,7 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.bundles.android.test) } + +fun getApiKey(propertyKey: String): String { + return gradleLocalProperties(rootDir).getProperty(propertyKey) +} diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginFragment.kt index 50077397..0a8d4f03 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginFragment.kt @@ -10,7 +10,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R @@ -21,8 +23,12 @@ import com.nextroom.nextroom.presentation.extension.snackbar import com.nextroom.nextroom.presentation.extension.toast import com.nextroom.nextroom.presentation.ui.login.compose.EmailLoginScreen import com.nextroom.nextroom.presentation.ui.onboarding.LoginFragment.Companion.SIGNUP_REQUEST_KEY +import com.nextroom.nextroom.presentation.util.GoogleAuthClient +import com.nextroom.nextroom.presentation.util.Logger import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException @AndroidEntryPoint class EmailLoginFragment : ComposeBaseViewModelFragment() { @@ -30,6 +36,9 @@ class EmailLoginFragment : ComposeBaseViewModelFragment() { override val screenName = "email_login" override val viewModel: EmailLoginViewModel by viewModels() + @Inject + lateinit var googleAuthClient: GoogleAuthClient + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -45,7 +54,7 @@ class EmailLoginFragment : ComposeBaseViewModelFragment() { onPasswordChange = viewModel::inputPassword, onEmailSaveCheckedChange = viewModel::onEmailSaveChecked, onLoginClick = viewModel::complete, - onGoogleLoginClick = viewModel::requestGoogleAuth, + onGoogleLoginClick = ::requestGoogleAuth, onBackClick = { findNavController().navigateUp() }, onCustomerServiceClick = ::openCustomerService, onSignupClick = ::openSignupWebView, @@ -67,6 +76,34 @@ class EmailLoginFragment : ComposeBaseViewModelFragment() { } } + /** + * Credential Manager는 계정 선택 UI를 띄우기 위해 Activity를 필요로 한다. + * Activity 참조가 화면 밖으로 새어나가지 않도록 viewLifecycleOwner 스코프 안에서만 다룬다. + */ + private fun requestGoogleAuth() { + viewLifecycleOwner.lifecycleScope.launch { + // 계정 선택 UI가 뜨기까지 시간이 걸리므로 요청 시작 시점부터 로딩을 노출한다. + viewModel.setGoogleAuthLoading(true) + val idToken = try { + googleAuthClient.requestGoogleIdToken(requireActivity()) + } catch (e: Exception) { + // 성공하면 loginWithGoogle이 로딩을 이어받으므로, 실패한 경우에만 해제한다. + viewModel.setGoogleAuthLoading(false) + when (e) { + // 사용자가 계정 선택을 취소한 경우이므로 오류로 처리하지 않는다. + is GetCredentialCancellationException -> Unit + is CancellationException -> throw e + else -> { + Logger.e(e) + toast(R.string.error_something) + } + } + return@launch + } + viewModel.loginWithGoogle(idToken) + } + } + private fun handleEvent(event: EmailLoginViewModel.UiEvent) { when (event) { is EmailLoginViewModel.UiEvent.ShowMessage -> @@ -74,7 +111,6 @@ class EmailLoginFragment : ComposeBaseViewModelFragment() { is EmailLoginViewModel.UiEvent.EmailLoginFailed -> snackbar(event.message) - EmailLoginViewModel.UiEvent.GoogleAuthFailed, EmailLoginViewModel.UiEvent.GoogleLoginFailed -> toast(R.string.error_something) is EmailLoginViewModel.UiEvent.NeedAdditionalUserInfo -> moveToSignup() diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginViewModel.kt index eaf0037d..2244df79 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/login/EmailLoginViewModel.kt @@ -1,7 +1,6 @@ package com.nextroom.nextroom.presentation.ui.login import androidx.annotation.StringRes -import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.lifecycle.viewModelScope import com.nextroom.nextroom.domain.model.Result import com.nextroom.nextroom.domain.model.onFailure @@ -94,23 +93,19 @@ class EmailLoginViewModel @Inject constructor( } } - fun requestGoogleAuth() { - baseViewModelScope.launch { - _uiState.update { it.copy(loading = true) } - try { - adminRepository.requestGoogleAuth().getOrThrow - .also { postGoogleLogin(it.idToken) } - } catch (e: GetCredentialCancellationException) { - // do nothing - } catch (e: Exception) { - _uiEvent.emit(UiEvent.GoogleAuthFailed) - } - _uiState.update { it.copy(loading = false) } - } + /** + * 계정 선택 UI가 뜨기 전까지도 로딩을 노출하기 위해, 자격증명을 요청하는 Fragment가 로딩 시작/해제를 알린다. + */ + fun setGoogleAuthLoading(loading: Boolean) { + _uiState.update { it.copy(loading = loading) } } - private fun postGoogleLogin(idToken: String) { + /** + * 구글 계정 선택은 Activity가 필요하므로 Fragment가 담당하고, 여기서는 발급받은 id token으로 로그인만 처리한다. + */ + fun loginWithGoogle(idToken: String) { baseViewModelScope.launch { + _uiState.update { it.copy(loading = true) } try { adminRepository.postGoogleLogin(idToken).getOrThrow.let { if (!it.isComplete) { @@ -120,6 +115,7 @@ class EmailLoginViewModel @Inject constructor( } catch (e: Exception) { _uiEvent.emit(UiEvent.GoogleLoginFailed) } + _uiState.update { it.copy(loading = false) } } } @@ -138,7 +134,6 @@ class EmailLoginViewModel @Inject constructor( sealed interface UiEvent { data class EmailLoginFailed(val message: String) : UiEvent data class ShowMessage(val message: UiText) : UiEvent - data object GoogleAuthFailed : UiEvent data object GoogleLoginFailed : UiEvent data class NeedAdditionalUserInfo(val shopName: String?) : UiEvent } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginFragment.kt index 280b0453..54cb1af4 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginFragment.kt @@ -8,21 +8,29 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.presentation.base.ComposeBaseViewModelFragment import com.nextroom.nextroom.presentation.extension.repeatOnStarted import com.nextroom.nextroom.presentation.extension.safeNavigate import com.nextroom.nextroom.presentation.ui.onboarding.compose.LoginScreen +import com.nextroom.nextroom.presentation.util.GoogleAuthClient import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException @AndroidEntryPoint class LoginFragment : ComposeBaseViewModelFragment() { override val screenName = "login" override val viewModel: LoginViewModel by viewModels() + @Inject + lateinit var googleAuthClient: GoogleAuthClient + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -34,7 +42,7 @@ class LoginFragment : ComposeBaseViewModelFragment() { val isLoading by viewModel.apiLoading.collectAsState() LoginScreen( isLoading = isLoading, - onGoogleLoginClick = viewModel::requestGoogleAuth, + onGoogleLoginClick = ::requestGoogleAuth, onEmailLoginClick = ::moveToEmailLogin, onTryWithoutLoginClick = ::moveToTutorial, ) @@ -67,6 +75,31 @@ class LoginFragment : ComposeBaseViewModelFragment() { } } + /** + * Credential Manager는 계정 선택 UI를 띄우기 위해 Activity를 필요로 한다. + * Activity 참조가 화면 밖으로 새어나가지 않도록 viewLifecycleOwner 스코프 안에서만 다룬다. + */ + private fun requestGoogleAuth() { + viewLifecycleOwner.lifecycleScope.launch { + // 계정 선택 UI가 뜨기까지 시간이 걸리므로 요청 시작 시점부터 로딩을 노출한다. + viewModel.setGoogleAuthLoading(true) + val idToken = try { + googleAuthClient.requestGoogleIdToken(requireActivity()) + } catch (e: Exception) { + // 성공하면 loginWithGoogle이 로딩을 이어받으므로, 실패한 경우에만 해제한다. + viewModel.setGoogleAuthLoading(false) + when (e) { + // 사용자가 계정 선택을 취소한 경우이므로 오류로 처리하지 않는다. + is GetCredentialCancellationException -> Unit + is CancellationException -> throw e + else -> viewModel.handleError(e) + } + return@launch + } + viewModel.loginWithGoogle(idToken) + } + } + private fun moveToThemeSelect() { LoginFragmentDirections.moveToThemeSelect().also { findNavController().safeNavigate(it) } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginViewModel.kt index a7abc9c0..4efd3f64 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/onboarding/LoginViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,28 +24,26 @@ class LoginViewModel @Inject constructor( false, ) - val apiLoading = MutableStateFlow(false) + private val _apiLoading = MutableStateFlow(false) + val apiLoading: StateFlow = _apiLoading.asStateFlow() private val _uiEvent = MutableSharedFlow() val uiEvent = _uiEvent.asSharedFlow() - fun requestGoogleAuth() { - baseViewModelScope.launch { - try { - apiLoading.emit(true) - adminRepository.requestGoogleAuth().getOrThrow - .also { postGoogleLogin(it.idToken) } - } catch (e: Exception) { - handleError(e) - } finally { - apiLoading.emit(false) - } - } + /** + * 계정 선택 UI가 뜨기 전까지도 로딩을 노출하기 위해, 자격증명을 요청하는 Fragment가 로딩 시작/해제를 알린다. + */ + fun setGoogleAuthLoading(loading: Boolean) { + _apiLoading.value = loading } - private fun postGoogleLogin(idToken: String) { + /** + * 구글 계정 선택은 Activity가 필요하므로 Fragment가 담당하고, 여기서는 발급받은 id token으로 로그인만 처리한다. + */ + fun loginWithGoogle(idToken: String) { baseViewModelScope.launch { try { + _apiLoading.emit(true) adminRepository.postGoogleLogin(idToken).getOrThrow.let { if (!it.isComplete) { _uiEvent.emit(UIEvent.NeedAdditionalUserInfo(it.shopName)) @@ -52,6 +51,8 @@ class LoginViewModel @Inject constructor( } } catch (e: Exception) { handleError(e) + } finally { + _apiLoading.emit(false) } } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/util/GoogleAuthClient.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/util/GoogleAuthClient.kt new file mode 100644 index 00000000..094f197a --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/util/GoogleAuthClient.kt @@ -0,0 +1,63 @@ +package com.nextroom.nextroom.presentation.util + +import android.app.Activity +import android.content.Context +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException +import com.nextroom.nextroom.presentation.BuildConfig +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 구글 계정 선택 UI를 띄우고 id token을 받아온다. + * + * Credential Manager는 선택 UI를 띄우기 위해 반드시 Activity 기반 Context를 필요로 한다. + * Application Context를 넘기면 Android 14 미만 기기에서 Play Services 폴백 경로를 타면서 + * "Failed to launch the selector UI" 오류가 발생하므로 [requestGoogleIdToken]에 Activity를 전달해야 한다. + */ +@Singleton +class GoogleAuthClient @Inject constructor( + @ApplicationContext context: Context, +) { + private val credentialManager = CredentialManager.create(context) + + private val request: GetCredentialRequest by lazy { + val googleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) + .setServerClientId(BuildConfig.O_AUTH_WEB_CLIENT_ID) + .build() + + GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + } + + suspend fun requestGoogleIdToken(activity: Activity): String { + val response = credentialManager.getCredential( + context = activity, + request = request, + ) + return response.extractIdToken() + } + + private fun GetCredentialResponse.extractIdToken(): String { + val credential = credential + if (credential !is CustomCredential || + credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL + ) { + throw IllegalStateException("unexpected type of credential") + } + + return try { + GoogleIdTokenCredential.createFrom(credential.data).idToken + } catch (e: GoogleIdTokenParsingException) { + throw IllegalStateException("received an invalid google id token response", e) + } + } +}