diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d342e89..7ef020bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,9 @@ android { val kakaoNativeAppKey = properties["kakao.native.app.key"].toString() buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoNativeAppKey\"") manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoNativeAppKey + + val googleClientID = properties["google.web.client.id"].toString() + buildConfigField("String", "GOOGLE_CLIENT_ID", "\"$googleClientID\"") } signingConfigs { @@ -125,6 +128,11 @@ dependencies { // calendar implementation(libs.kizitonwose.calendar) + + // Google + implementation(libs.androidx.credentials) + implementation(libs.androidx.credentials.play.services.auth) + implementation(libs.googleid) } ktlint { diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/login/GoogleLoginManager.kt b/app/src/main/java/org/sopt/certi/presentation/ui/login/GoogleLoginManager.kt new file mode 100644 index 00000000..b5e9d0ba --- /dev/null +++ b/app/src/main/java/org/sopt/certi/presentation/ui/login/GoogleLoginManager.kt @@ -0,0 +1,82 @@ +package org.sopt.certi.presentation.ui.login + +import android.app.Activity +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException +import androidx.credentials.CustomCredential +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.sopt.certi.BuildConfig +import javax.inject.Inject + +class GoogleLoginManager @Inject constructor() { + companion object { + const val WEB_CLIENT_ID = BuildConfig.GOOGLE_CLIENT_ID + } + + fun login( + activity: Activity, + onResult: (Result) -> Unit + ) { + val credentialManager = CredentialManager.create(activity) + + val googleIdOption = GetGoogleIdOption.Builder() + .setServerClientId(WEB_CLIENT_ID) + .setFilterByAuthorizedAccounts(false) + .setAutoSelectEnabled(true) + .build() + + val request = GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + + CoroutineScope(Dispatchers.Main).launch { + try { + val response = credentialManager.getCredential( + request = request, + context = activity + ) + onResult(handleResponse(response)) + } catch (e: NoCredentialException) { + onResult(Result.failure(e)) + } catch (e: GetCredentialException) { + onResult(Result.failure(e)) + } catch (e: Throwable) { + onResult(Result.failure(e)) + } + } + } + + private fun handleResponse(response: GetCredentialResponse): Result { + return try { + val credential = response.credential + + when (credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + val googleCred = GoogleIdTokenCredential.createFrom(credential.data) + val idToken = googleCred.idToken + Result.success(idToken) + } else { + Result.failure(IllegalStateException("Unexpected credential type: ${credential.type}")) + } + } + + else -> { + Result.failure(IllegalStateException("Unexpected credential class: ${credential::class.java.name}")) + } + } + } catch (e: GoogleIdTokenParsingException) { + Result.failure(e) + } catch (e: Throwable) { + Result.failure(e) + } + } +} diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginScreen.kt b/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginScreen.kt index be2caa9c..fff3f398 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import org.sopt.certi.R +import org.sopt.certi.core.util.findActivity import org.sopt.certi.core.util.heightForScreenPercentage import org.sopt.certi.core.util.screenHeightDp import org.sopt.certi.core.util.screenWidthDp @@ -38,9 +39,11 @@ fun LoginRoute( navigateToOnBoarding: () -> Unit, navigateToHome: () -> Unit, viewModel: LoginViewModel = hiltViewModel(), - kakaoLoginManager: KakaoLoginManager = KakaoLoginManager() + kakaoLoginManager: KakaoLoginManager = KakaoLoginManager(), + googleLoginManager: GoogleLoginManager = GoogleLoginManager() ) { val context = LocalContext.current + val activity = context.findActivity() LoginScreen( padding = padding, @@ -65,7 +68,23 @@ fun LoginRoute( } } }, - onGoogleLoginClick = { navigateToHome() } + onGoogleLoginClick = { + val safeActivity = activity + + googleLoginManager.login(safeActivity) { result -> + result.onSuccess { idToken -> + viewModel.signInWithGoogleIdToken( + idToken = idToken, + onSuccess = { needSignUp -> + if (needSignUp) navigateToOnBoarding() else navigateToHome() + }, + onFailure = { error -> Timber.e(error) } + ) + }.onFailure { error -> + Timber.e(error) + } + } + } ) } diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginViewModel.kt index 4c6153bb..62897395 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/login/LoginViewModel.kt @@ -43,4 +43,31 @@ class LoginViewModel @Inject constructor( ) } } + + fun signInWithGoogleIdToken( + idToken: String, + onSuccess: (needSignUp: Boolean) -> Unit, + onFailure: (Throwable) -> Unit + ) { + viewModelScope.launch { + signInUseCase(idToken, SocialLoginType.GOOGLE.name.lowercase(Locale.ROOT)).fold( + onSuccess = { result -> + onSuccess(result.needSignUp) + if (result.needSignUp) { + tokenManager.savePreSignupToken(result.preSignupToken) + tokenManager.saveUserInformation(result.userInformation) + } else { + result.jwtResponse?.let { + tokenManager.saveToken(it.accessToken) + tokenManager.saveRefreshToken(it.refreshToken) + } + } + }, + onFailure = { + Timber.e(it) + onFailure(it) + } + ) + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db3f5195..86dbc520 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,7 +26,7 @@ 욕설과 비속어 등은 사용할 수 없습니다. 닉네임 중복 확인 %s님, - 이제 따와 함께 해요! + 이제 따요와 함께 해요! 최종학력 학과 희망직무 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f07b4054..7d9ee61e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,8 @@ accompanist = "0.25.1" # Core coreKtx = "1.16.0" appcompat = "1.7.0" +credentials = "1.5.0" +googleid = "1.2.0" lifecycleRuntimeKtx = "2.8.7" # Kotlin & Tools @@ -55,12 +57,15 @@ calendar = "2.6.0" [libraries] # Core androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "credentials" } +androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentials" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } # Kotlin +googleid = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "googleid" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" }