Skip to content

Commit 494a7db

Browse files
committed
[BOOK-474] feat: 구글 로그인 구현
구글로 부터 idToken을 받아와 서버에 전달하는 로직
1 parent 67fd699 commit 494a7db

File tree

14 files changed

+227
-11
lines changed

14 files changed

+227
-11
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ android {
3232
getByName("debug") {
3333
isDebuggable = true
3434
applicationIdSuffix = ".dev"
35+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("DEBUG_GOOGLE_WEB_CLIENT_ID"))
3536
manifestPlaceholders += mapOf(
3637
"appName" to "@string/app_name_dev",
3738
)
@@ -42,6 +43,7 @@ android {
4243
isMinifyEnabled = true
4344
isShrinkResources = true
4445
signingConfig = signingConfigs.getByName("release")
46+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("RELEASE_GOOGLE_WEB_CLIENT_ID"))
4547
manifestPlaceholders += mapOf(
4648
"appName" to "@string/app_name",
4749
)

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import com.ninecraft.booket.core.model.UserState
55
import kotlinx.coroutines.flow.Flow
66

77
interface AuthRepository {
8-
suspend fun login(accessToken: String): Result<Unit>
8+
suspend fun login(
9+
providerType: String,
10+
token: String,
11+
): Result<Unit>
912

1013
suspend fun logout(): Result<Unit>
1114

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ import com.ninecraft.booket.core.di.DataScope
1212
import dev.zacsweers.metro.SingleIn
1313
import kotlinx.coroutines.flow.map
1414

15-
private const val KAKAO_PROVIDER_TYPE = "KAKAO"
16-
1715
@SingleIn(DataScope::class)
1816
@Inject
1917
class DefaultAuthRepository(
2018
private val service: ReedService,
2119
private val tokenDataSource: TokenDataSource,
2220
) : AuthRepository {
23-
override suspend fun login(accessToken: String) = runSuspendCatching {
21+
override suspend fun login(
22+
providerType: String,
23+
token: String,
24+
) = runSuspendCatching {
2425
val response = service.login(
2526
LoginRequest(
26-
providerType = KAKAO_PROVIDER_TYPE,
27-
oauthToken = accessToken,
27+
providerType = providerType,
28+
oauthToken = token,
2829
),
2930
)
3031
saveTokens(response.accessToken, response.refreshToken)

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@ import androidx.compose.foundation.BorderStroke
44
import androidx.compose.runtime.Composable
55
import androidx.compose.ui.graphics.Color
66
import androidx.compose.ui.unit.dp
7+
import com.ninecraft.booket.core.designsystem.theme.Google
78
import com.ninecraft.booket.core.designsystem.theme.Kakao
89
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
910

1011
enum class ReedButtonColorStyle {
11-
PRIMARY, SECONDARY, TERTIARY, STROKE, TEXT, KAKAO;
12+
PRIMARY, SECONDARY, TERTIARY, STROKE, TEXT, KAKAO, GOOGLE;
1213

1314
@Composable
1415
fun containerColor(isPressed: Boolean) = when (this) {
1516
PRIMARY -> if (isPressed) ReedTheme.colors.bgPrimaryPressed else ReedTheme.colors.bgPrimary
1617
SECONDARY -> if (isPressed) ReedTheme.colors.bgSecondaryPressed else ReedTheme.colors.bgSecondary
1718
TERTIARY -> if (isPressed) ReedTheme.colors.bgTertiaryPressed else ReedTheme.colors.bgTertiary
18-
STROKE -> if (isPressed) ReedTheme.colors.basePrimary else ReedTheme.colors.basePrimary
19+
STROKE -> ReedTheme.colors.basePrimary
1920
TEXT -> Color.Transparent
2021
KAKAO -> Kakao
22+
GOOGLE -> ReedTheme.colors.basePrimary
2123
}
2224

2325
@Composable
@@ -28,6 +30,7 @@ enum class ReedButtonColorStyle {
2830
STROKE -> ReedTheme.colors.contentBrand
2931
TEXT -> ReedTheme.colors.borderBrand
3032
KAKAO -> ReedTheme.colors.contentPrimary
33+
GOOGLE -> ReedTheme.colors.contentPrimary
3134
}
3235

3336
@Composable

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ val Blue800 = Color(0xFF1269EC)
6464
val Blue900 = Color(0xFF1F47CD)
6565

6666
val Kakao = Color(0xFFFBD300)
67+
val Google = Color(0xFF4285F4)
6768
val Blank = Color(0xFFD6D6D6)
6869
val HomeBg = Color(0xFFF0F9E8)
6970

feature/login/build.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
22

3+
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
4+
35
plugins {
46
alias(libs.plugins.booket.android.feature)
57
alias(libs.plugins.kotlin.serialization)
@@ -8,11 +10,32 @@ plugins {
810

911
android {
1012
namespace = "com.ninecraft.booket.feature.login"
13+
14+
buildTypes {
15+
getByName("debug") {
16+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("DEBUG_GOOGLE_WEB_CLIENT_ID"))
17+
}
18+
19+
getByName("release") {
20+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("RELEASE_GOOGLE_WEB_CLIENT_ID"))
21+
}
22+
}
23+
24+
buildFeatures {
25+
buildConfig = true
26+
}
1127
}
1228

1329
dependencies {
1430
implementations(
1531
libs.logger,
1632
libs.kakao.auth,
33+
libs.androidx.credentials,
34+
libs.androidx.credentials.play.services.auth,
35+
libs.googleid,
1736
)
1837
}
38+
39+
fun getApiKey(propertyKey: String): String {
40+
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
41+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.ninecraft.booket.feature.login
2+
3+
import android.content.Context
4+
import androidx.credentials.CredentialManager
5+
import androidx.credentials.CustomCredential
6+
import androidx.credentials.GetCredentialRequest
7+
import androidx.credentials.GetCredentialResponse
8+
import androidx.credentials.exceptions.GetCredentialCancellationException
9+
import androidx.credentials.exceptions.GetCredentialException
10+
import androidx.credentials.exceptions.NoCredentialException
11+
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
12+
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
13+
import com.ninecraft.booket.core.designsystem.R as designR
14+
import com.orhanobut.logger.Logger
15+
import dev.zacsweers.metro.Inject
16+
17+
@Inject
18+
internal class GoogleLoginClient {
19+
suspend fun loginWithGoogle(
20+
context: Context,
21+
webClientId: String,
22+
onSuccess: (String) -> Unit,
23+
onFailure: (String) -> Unit,
24+
) {
25+
val credentialManager = CredentialManager.create(context)
26+
27+
val googleIdOption = GetGoogleIdOption.Builder()
28+
.setFilterByAuthorizedAccounts(false)
29+
.setServerClientId(webClientId)
30+
.setAutoSelectEnabled(true)
31+
.build()
32+
33+
val credentialRequest = GetCredentialRequest.Builder()
34+
.addCredentialOption(googleIdOption)
35+
.build()
36+
37+
try {
38+
val result = credentialManager.getCredential(
39+
request = credentialRequest,
40+
context = context,
41+
)
42+
handleSignIn(result, onSuccess, onFailure, context)
43+
} catch (e: GetCredentialCancellationException) {
44+
Logger.e("Google 로그인 취소됨")
45+
onFailure(context.getString(designR.string.unknown_error_message))
46+
} catch (e: NoCredentialException) {
47+
Logger.e("Google 계정을 찾을 수 없음")
48+
onFailure(context.getString(designR.string.unknown_error_message))
49+
} catch (e: GetCredentialException) {
50+
Logger.e("Google 로그인 실패: ${e.message}")
51+
onFailure(context.getString(designR.string.network_error_message))
52+
} catch (e: Exception) {
53+
Logger.e("알 수 없는 오류: ${e.message}")
54+
onFailure(context.getString(designR.string.unknown_error_message))
55+
}
56+
}
57+
58+
private fun handleSignIn(
59+
result: GetCredentialResponse,
60+
onSuccess: (String) -> Unit,
61+
onFailure: (String) -> Unit,
62+
context: Context,
63+
) {
64+
val credential = result.credential
65+
66+
if (credential is CustomCredential &&
67+
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
68+
try {
69+
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
70+
val idToken = googleIdTokenCredential.idToken
71+
Logger.d("Google 로그인 성공: ${googleIdTokenCredential.id}")
72+
onSuccess(idToken)
73+
} catch (e: Exception) {
74+
Logger.e("Google ID Token 파싱 실패: ${e.message}")
75+
onFailure(context.getString(designR.string.unknown_error_message))
76+
}
77+
} else {
78+
Logger.e("예상치 못한 credential type: ${credential.type}")
79+
onFailure(context.getString(designR.string.unknown_error_message))
80+
}
81+
}
82+
}

feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,60 @@ package com.ninecraft.booket.feature.login
33
import android.widget.Toast
44
import androidx.compose.runtime.Composable
55
import androidx.compose.runtime.remember
6+
import androidx.compose.runtime.rememberCoroutineScope
67
import androidx.compose.ui.platform.LocalContext
78
import com.skydoves.compose.effects.RememberedEffect
9+
import kotlinx.coroutines.launch
810

911
@Composable
1012
internal fun HandleLoginSideEffects(
1113
state: LoginUiState,
1214
eventSink: (LoginUiEvent) -> Unit,
1315
) {
1416
val context = LocalContext.current
17+
val scope = rememberCoroutineScope()
1518
val kakaoLoginClient = remember { KakaoLoginClient() }
19+
val googleLoginClient = remember { GoogleLoginClient() }
1620

1721
RememberedEffect(state.sideEffect) {
1822
when (state.sideEffect) {
1923
is LoginSideEffect.KakaoLogin -> {
2024
kakaoLoginClient.loginWithKakao(
2125
context = context,
2226
onSuccess = { token ->
23-
eventSink(LoginUiEvent.Login(token))
27+
eventSink(
28+
LoginUiEvent.Login(
29+
providerType = LoginUiEvent.PROVIDER_TYPE_KAKAO,
30+
token = token,
31+
),
32+
)
2433
},
2534
onFailure = { errorMessage ->
2635
eventSink(LoginUiEvent.LoginFailure(errorMessage))
2736
},
2837
)
2938
}
3039

40+
is LoginSideEffect.GoogleLogin -> {
41+
scope.launch {
42+
googleLoginClient.loginWithGoogle(
43+
context = context,
44+
webClientId = BuildConfig.GOOGLE_WEB_CLIENT_ID,
45+
onSuccess = { idToken ->
46+
eventSink(
47+
LoginUiEvent.Login(
48+
providerType = LoginUiEvent.PROVIDER_TYPE_GOOGLE,
49+
token = idToken,
50+
)
51+
)
52+
},
53+
onFailure = { errorMessage ->
54+
eventSink(LoginUiEvent.LoginFailure(errorMessage))
55+
},
56+
)
57+
}
58+
}
59+
3160
is LoginSideEffect.ShowToast -> {
3261
Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show()
3362
}

feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ class LoginPresenter(
8585
sideEffect = LoginSideEffect.KakaoLogin()
8686
}
8787

88+
is LoginUiEvent.OnGoogleLoginButtonClick -> {
89+
isLoading = true
90+
sideEffect = LoginSideEffect.GoogleLogin()
91+
}
92+
8893
is LoginUiEvent.LoginFailure -> {
8994
isLoading = false
9095
analyticsHelper.logEvent(EVENT_ERROR_LOGIN)
@@ -95,7 +100,7 @@ class LoginPresenter(
95100
scope.launch {
96101
try {
97102
isLoading = true
98-
authRepository.login(event.accessToken)
103+
authRepository.login(event.providerType, event.token)
99104
.onSuccess {
100105
userRepository.syncFcmToken()
101106
navigateAfterLogin()

feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,28 @@ internal fun LoginUi(
113113
)
114114
},
115115
)
116+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
117+
ReedButton(
118+
onClick = {
119+
state.eventSink(LoginUiEvent.OnGoogleLoginButtonClick)
120+
},
121+
sizeStyle = largeButtonStyle,
122+
colorStyle = ReedButtonColorStyle.GOOGLE,
123+
modifier = Modifier
124+
.fillMaxWidth()
125+
.padding(
126+
start = ReedTheme.spacing.spacing5,
127+
end = ReedTheme.spacing.spacing5,
128+
),
129+
text = stringResource(id = R.string.google_login),
130+
leadingIcon = {
131+
Icon(
132+
imageVector = ImageVector.vectorResource(id = R.drawable.ic_google),
133+
contentDescription = "Google Icon",
134+
tint = Color.Unspecified,
135+
)
136+
}
137+
)
116138
Spacer(
117139
modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing2 else ReedTheme.spacing.spacing8),
118140
)

0 commit comments

Comments
 (0)