Skip to content

Commit b24f731

Browse files
authored
Merge pull request #1 from Noostak/feature/#84-google-login
[Fix/#84] : 구글 로그인 오류 해결
2 parents af139f2 + c876db5 commit b24f731

9 files changed

Lines changed: 166 additions & 57 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ android {
3131
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
3232
buildConfigField("String", "KAKAO_API_KEY", properties["KAKAO_API_KEY"].toString())
3333
buildConfigField("String", "GOOGLE_CLIENT_ID", properties["GOOGLE_CLIENT_ID"].toString())
34+
buildConfigField("String", "GOOGLE_CLIENT_SECRET", properties["GOOGLE_CLIENT_SECRET"].toString())
3435
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = properties["KAKAO_NATIVE_APP_KEY"].toString()
3536
}
3637

app/src/main/java/com/sopt/noostak/di/GoogleIdModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,11 @@ object GoogleIdModule {
1717
fun provideGoogleClientId(): String {
1818
return BuildConfig.GOOGLE_CLIENT_ID
1919
}
20+
21+
@Provides
22+
@Singleton
23+
@Named("GoogleClientSecret")
24+
fun provideGoogleClientSecret(): String {
25+
return BuildConfig.GOOGLE_CLIENT_SECRET
26+
}
2027
}

app/src/main/java/com/sopt/noostak/di/TokenInterceptor.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,18 @@ class TokenInterceptor @Inject constructor(
5151
val refreshToken = preferenceDatasource.refreshToken.first()
5252

5353
return try {
54-
val tokenResult = runBlocking(Dispatchers.IO) {
54+
val tokenResult = withContext(Dispatchers.IO) {
5555
authService.postReissueToken(refreshToken)
5656
}
57-
when (tokenResult.status == SUCCESS) {
58-
true -> {
59-
preferenceDatasource.updateAccessToken(
60-
BEARER + tokenResult.result?.accessToken
61-
)
62-
true
63-
}
6457

65-
false -> false
58+
if (tokenResult.status == SUCCESS && tokenResult.result != null) {
59+
tokenResult.result?.let { result ->
60+
preferenceDatasource.updateAccessToken(BEARER + result.accessToken)
61+
preferenceDatasource.updateRefreshToken(BEARER + result.refreshToken)
62+
}
63+
true
64+
} else {
65+
false
6666
}
6767
} catch (e: Exception) {
6868
false
@@ -103,7 +103,7 @@ class TokenInterceptor @Inject constructor(
103103
).build()
104104

105105
companion object {
106-
const val SUCCESS = 200
106+
const val SUCCESS = 201
107107
const val CODE_TOKEN_EXPIRE = 401
108108
const val AUTHORIZATION = "Authorization"
109109
const val BEARER = "Bearer "

data/src/main/java/com/sopt/data/dto/response/ResponsePostReissueTokenDto.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable
77
data class ResponsePostReissueTokenDto(
88
@SerialName("accessToken") val accessToken: String,
99
@SerialName("refreshToken") val refreshToken: String,
10-
@SerialName("authType") val authType: String
10+
@SerialName("authId") val authId: String
1111
)

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ kakao = "2.20.1"
5656
# Google
5757
google-id = "1.1.1"
5858
play-services-auth = "21.1.0"
59-
6059
viewpager-indicator = "5.0"
60+
credentials = "1.2.0-alpha02"
6161

6262
# Hilt
6363
hilt = "2.52"
@@ -164,6 +164,8 @@ kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao"
164164
# Google
165165
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" }
166166
google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "google-id" }
167+
credentials-core = { group = "androidx.credentials", name = "credentials", version.ref = "credentials" }
168+
credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentials" }
167169

168170
# Hilt
169171
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } # Hilt Android 라이브러리

presentation/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,6 @@ dependencies {
116116
// Google
117117
implementation(libs.play.services.auth)
118118
implementation(libs.google.id)
119+
implementation(libs.credentials.core)
120+
implementation(libs.credentials.play.services.auth)
119121
}

presentation/src/main/java/com/sopt/presentation/auth/login/LogInRoute.kt

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.sopt.presentation.auth.login
22

3+
import android.app.Activity
4+
import androidx.activity.compose.rememberLauncherForActivityResult
5+
import androidx.activity.result.contract.ActivityResultContracts
36
import androidx.compose.animation.core.Animatable
47
import androidx.compose.foundation.Image
8+
import androidx.compose.foundation.background
59
import androidx.compose.foundation.layout.Column
610
import androidx.compose.foundation.layout.Spacer
711
import androidx.compose.foundation.layout.fillMaxSize
@@ -24,13 +28,19 @@ import androidx.compose.ui.tooling.preview.Preview
2428
import androidx.compose.ui.unit.dp
2529
import androidx.hilt.navigation.compose.hiltViewModel
2630
import androidx.lifecycle.compose.collectAsStateWithLifecycle
31+
import com.google.android.gms.auth.api.signin.GoogleSignIn
32+
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
33+
import com.google.android.gms.common.api.ApiException
2734
import com.sopt.core.designsystem.component.dialog.NoostakDialog
2835
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
2936
import com.sopt.core.designsystem.theme.NoostakTheme
3037
import com.sopt.core.extension.toast
3138
import com.sopt.core.type.DialogType
3239
import com.sopt.presentation.R
3340
import com.sopt.presentation.auth.component.LoginButton
41+
import kotlinx.coroutines.CoroutineScope
42+
import kotlinx.coroutines.Dispatchers
43+
import kotlinx.coroutines.launch
3444

3545
@Composable
3646
fun LoginRoute(
@@ -40,6 +50,38 @@ fun LoginRoute(
4050
) {
4151
val context = LocalContext.current
4252
val showDialog by loginViewModel.showDialog.collectAsStateWithLifecycle()
53+
val googleSignInIntent by loginViewModel.googleSignInIntent.collectAsStateWithLifecycle()
54+
55+
val googleSignInLauncher = rememberLauncherForActivityResult(
56+
ActivityResultContracts.StartActivityForResult()
57+
) { result ->
58+
if (result.resultCode != Activity.RESULT_OK) {
59+
loginViewModel.showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
60+
return@rememberLauncherForActivityResult
61+
}
62+
63+
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
64+
CoroutineScope(Dispatchers.Main).launch {
65+
try {
66+
val account: GoogleSignInAccount = task.getResult(ApiException::class.java)
67+
val authCode = account.serverAuthCode
68+
if (!authCode.isNullOrBlank()) {
69+
loginViewModel.exchangeAuthCodeForAccessToken(authCode)
70+
} else {
71+
loginViewModel.showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
72+
}
73+
} catch (e: Exception) {
74+
loginViewModel.showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
75+
}
76+
}
77+
loginViewModel.clearGoogleSignInIntent()
78+
}
79+
80+
LaunchedEffect(googleSignInIntent) {
81+
googleSignInIntent?.let { intent ->
82+
googleSignInLauncher.launch(intent)
83+
}
84+
}
4385

4486
LaunchedEffect(loginViewModel.sideEffects) {
4587
loginViewModel.sideEffects.collect { sideEffect ->
@@ -63,7 +105,10 @@ fun LoginRoute(
63105
loginViewModel.showDialog(dialogType, false)
64106
when (dialogType) {
65107
DialogType.NETWORK_LOGIN_KAKAO_FAILURE -> loginViewModel.kakaoLogin(context)
66-
DialogType.NETWORK_LOGIN_GOOGLE_FAILURE -> loginViewModel.googleLogin(context)
108+
DialogType.NETWORK_LOGIN_GOOGLE_FAILURE -> loginViewModel.prepareGoogleSignInIntent(
109+
context
110+
)
111+
67112
else -> Unit
68113
}
69114
},
@@ -74,7 +119,7 @@ fun LoginRoute(
74119

75120
LoginScreen(
76121
onKakaoLoginClick = { loginViewModel.kakaoLogin(context) },
77-
onGoogleLoginClick = { loginViewModel.googleLogin(context) }
122+
onGoogleLoginClick = { loginViewModel.prepareGoogleSignInIntent(context) }
78123
)
79124
}
80125

@@ -88,6 +133,7 @@ fun LoginScreen(
88133
Column(
89134
modifier = Modifier
90135
.fillMaxSize()
136+
.background(NoostakTheme.colors.blue600)
91137
.statusBarsPadding()
92138
.navigationBarsPadding()
93139
.padding(dimensionResource(R.dimen.horizontal_padding)),
@@ -122,8 +168,8 @@ private fun SocialLoginBottom(
122168
) {
123169
Text(
124170
text = stringResource(R.string.tv_login_description),
125-
color = NoostakTheme.colors.gray900,
126-
style = NoostakTheme.typography.c3Regular,
171+
color = NoostakTheme.colors.white,
172+
style = NoostakTheme.typography.c3SemiBold,
127173
modifier = Modifier.padding(bottom = 8.dp)
128174
)
129175
LoginButton(

presentation/src/main/java/com/sopt/presentation/auth/login/LoginViewModel.kt

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package com.sopt.presentation.auth.login
22

33
import android.content.Context
4+
import android.content.Intent
45
import androidx.annotation.StringRes
5-
import androidx.credentials.Credential
6-
import androidx.credentials.CredentialManager
7-
import androidx.credentials.GetCredentialRequest
86
import androidx.lifecycle.viewModelScope
9-
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
10-
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
7+
import com.google.android.gms.auth.api.signin.GoogleSignIn
8+
import com.google.android.gms.auth.api.signin.GoogleSignInClient
9+
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
1110
import com.kakao.sdk.auth.model.OAuthToken
1211
import com.kakao.sdk.common.model.ClientError
1312
import com.kakao.sdk.common.model.ClientErrorCause
@@ -19,28 +18,50 @@ import com.sopt.domain.repository.UserInfoRepository
1918
import com.sopt.domain.usecase.PostSocialLoginUseCase
2019
import com.sopt.presentation.R
2120
import dagger.hilt.android.lifecycle.HiltViewModel
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.Dispatchers
2223
import kotlinx.coroutines.flow.MutableStateFlow
2324
import kotlinx.coroutines.flow.StateFlow
2425
import kotlinx.coroutines.flow.update
2526
import kotlinx.coroutines.launch
26-
import timber.log.Timber
27+
import okhttp3.FormBody
28+
import okhttp3.OkHttpClient
29+
import okhttp3.Request
30+
import org.json.JSONObject
2731
import javax.inject.Inject
2832
import javax.inject.Named
2933

3034
@HiltViewModel
3135
class LoginViewModel @Inject constructor(
3236
@Named("GoogleClientId") private val googleClientId: String,
37+
@Named("GoogleClientSecret") private val googleClientSecret: String,
3338
private val userInfoRepository: UserInfoRepository,
3439
private val postSocialLoginUseCase: PostSocialLoginUseCase
3540
) : BaseViewModel<LoginSideEffect>() {
3641

3742
private val _showDialog = MutableStateFlow(Pair(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, false))
3843
val showDialog: StateFlow<Pair<DialogType, Boolean>> get() = _showDialog
3944

45+
private val _googleSignInIntent = MutableStateFlow<Intent?>(null)
46+
val googleSignInIntent: StateFlow<Intent?> get() = _googleSignInIntent
47+
4048
fun showDialog(dialogType: DialogType, isVisible: Boolean) {
4149
_showDialog.update { it.copy(first = dialogType, second = isVisible) }
4250
}
4351

52+
fun prepareGoogleSignInIntent(context: Context) {
53+
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
54+
.requestEmail()
55+
.requestServerAuthCode(googleClientId, true)
56+
.build()
57+
val client: GoogleSignInClient = GoogleSignIn.getClient(context, gso)
58+
_googleSignInIntent.value = client.signInIntent
59+
}
60+
61+
fun clearGoogleSignInIntent() {
62+
_googleSignInIntent.value = null
63+
}
64+
4465
// Kakao Login
4566
fun kakaoLogin(context: Context) {
4667
val loginCallback: (OAuthToken?, Throwable?) -> Unit = this::handleKakaoLoginResult
@@ -69,39 +90,45 @@ class LoginViewModel @Inject constructor(
6990
}
7091
}
7192

72-
// Google Login
73-
fun googleLogin(context: Context) {
74-
val credentialManager = CredentialManager.create(context)
75-
76-
val googleIdOption = GetGoogleIdOption.Builder()
77-
.setFilterByAuthorizedAccounts(false)
78-
.setServerClientId(googleClientId)
79-
.setAutoSelectEnabled(true)
80-
.build()
81-
82-
val request = GetCredentialRequest.Builder()
83-
.addCredentialOption(googleIdOption)
84-
.build()
85-
86-
viewModelScope.launch {
87-
runCatching {
88-
val result = credentialManager.getCredential(context, request)
89-
handleGoogleLoginResult(result.credential)
90-
}.onFailure { exception ->
91-
handleError(exception, R.string.toast_google_login_failed)
93+
// Get Google AccessToken
94+
fun exchangeAuthCodeForAccessToken(authCode: String) {
95+
CoroutineScope(Dispatchers.IO).launch {
96+
try {
97+
val requestBody = FormBody.Builder()
98+
.add("grant_type", "authorization_code")
99+
.add("code", authCode)
100+
.add("client_id", googleClientId)
101+
.add("client_secret", googleClientSecret)
102+
.add("redirect_uri", "")
103+
.build()
104+
105+
val request = Request.Builder()
106+
.url("https://oauth2.googleapis.com/token")
107+
.post(requestBody)
108+
.build()
109+
110+
val client = OkHttpClient()
111+
val response = client.newCall(request).execute()
112+
val jsonString = response.body?.string()
113+
val json = JSONObject(jsonString ?: "")
114+
115+
val accessToken = json.optString("access_token")
116+
117+
if (accessToken.isNotBlank()) {
118+
onGoogleAccessTokenReceived(accessToken)
119+
} else {
120+
showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
121+
}
122+
} catch (e: Exception) {
92123
showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
93124
}
94125
}
95126
}
96127

97-
private fun handleGoogleLoginResult(credential: Credential) {
98-
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
99-
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
100-
postSocialLogin(googleIdTokenCredential.id, GOOGLE)
101-
showToast(R.string.toast_google_login_success)
102-
} else {
103-
showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
104-
}
128+
// Google Login
129+
private fun onGoogleAccessTokenReceived(accessToken: String) {
130+
postSocialLogin(BEARER + accessToken, GOOGLE)
131+
showToast(R.string.toast_google_login_success)
105132
}
106133

107134
private fun handleError(error: Throwable?, @StringRes errorMessageResId: Int) {
@@ -134,7 +161,6 @@ class LoginViewModel @Inject constructor(
134161
},
135162
onFailure = { error ->
136163
emitSideEffect(LoginSideEffect.NavigateToOnboarding(accessToken, socialType))
137-
Timber.e("postSocialLogin Failed: ${error.message}")
138164
}
139165
)
140166
}

0 commit comments

Comments
 (0)