Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature/#17] : 카카오 로그인, 구글 로그인 구현 #23

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import java.util.Properties

plugins {
Expand Down Expand Up @@ -30,6 +29,9 @@ android {
useSupportLibrary = true
}
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
buildConfigField("String", "KAKAO_API_KEY", properties["KAKAO_API_KEY"].toString())
buildConfigField("String", "GOOGLE_CLIENT_ID", properties["GOOGLE_CLIENT_ID"].toString())
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = properties["KAKAO_NATIVE_APP_KEY"].toString()
}

buildTypes {
Expand Down Expand Up @@ -130,4 +132,8 @@ dependencies {
// Kakao
implementation(libs.kakao.all)
implementation(libs.kakao.user)

// Google
implementation(libs.play.services.auth)
implementation(libs.google.id)
}
18 changes: 15 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<application
android:name="com.sopt.noostak.MyApp"
Expand All @@ -28,6 +29,17 @@

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="oauth"
android:scheme="${KAKAO_NATIVE_APP_KEY}" />
</intent-filter>
</activity>
</application>

Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/sopt/noostak/MyApp.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sopt.noostak

import android.app.Application
import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

Expand All @@ -9,9 +10,14 @@ class MyApp : Application() {
override fun onCreate() {
super.onCreate()
setTimber()
initKakao()
}

private fun setTimber() {
Timber.plant(Timber.DebugTree())
}

private fun initKakao() {
KakaoSdk.init(this, BuildConfig.KAKAO_API_KEY)
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/sopt/noostak/di/AuthModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sopt.noostak.di

import com.sopt.noostak.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Named
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AuthModule {
@Provides
@Singleton
@Named("GoogleClientId")
fun provideGoogleClientId(): String {
return BuildConfig.GOOGLE_CLIENT_ID
}
}
8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ landscapist = "2.3.6"
# Kakao
kakao = "2.20.1"

# Google
google-id = "1.1.1"
play-services-auth = "21.1.0"

viewpager-indicator = "5.0"

# Hilt
Expand Down Expand Up @@ -155,6 +159,10 @@ landscapist-animation = { group = "com.github.skydoves", name = "landscapist-ani
kakao-all = { group = "com.kakao.sdk", name = "v2-all", version.ref = "kakao" } # Kakao SDK 라이브러리
kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } # Kakao 로그인 라이브러리

# Google
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" }
google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "google-id" }

# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } # Hilt Android 라이브러리
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" } # Hilt Core 라이브러리
Expand Down
4 changes: 4 additions & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,8 @@ dependencies {
// Kakao
implementation(libs.kakao.all)
implementation(libs.kakao.user)

// Google
implementation(libs.play.services.auth)
implementation(libs.google.id)
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package com.sopt.presentation.auth.login

import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.android.gms.auth.api.identity.Identity
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
import com.sopt.core.designsystem.theme.NoostakTheme
import com.sopt.core.extension.toast
import com.sopt.presentation.R
import com.sopt.presentation.auth.component.LoginButton

Expand All @@ -30,18 +38,37 @@ fun LoginRoute(
navigateToSignUp: (String) -> Unit,
loginViewModel: LoginViewModel = hiltViewModel()
) {
val context = LocalContext.current

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val credential = Identity.getSignInClient(context)
.getSignInCredentialFromIntent(result.data)
loginViewModel.handleGoogleLoginResult(credential)
} else {
context.toast(R.string.toast_login_cancelled)
}
}

LaunchedEffect(Unit) {
loginViewModel.initializeGoogleSignIn(context)
}

LaunchedEffect(loginViewModel.sideEffects) {
loginViewModel.sideEffects.collect { sideEffect ->
when (sideEffect) {
is LoginSideEffect.NavigateToHome -> navigateToHome()
is LoginSideEffect.NavigateSignUp -> navigateToSignUp(sideEffect.authId)
is LoginSideEffect.ShowToast -> context.toast(sideEffect.message)
}
}
}

LoginScreen(
onKakaoLoginClick = loginViewModel::kakaoLogin,
onGoogleLoginClick = loginViewModel::googleLogin
onKakaoLoginClick = { loginViewModel.kakaoLogin(context) },
onGoogleLoginClick = { loginViewModel.googleLogin(launcher) }
)
}

Expand All @@ -55,7 +82,9 @@ fun LoginScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.horizontal_padding)),
.padding(dimensionResource(R.dimen.horizontal_padding))
.statusBarsPadding()
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.sopt.presentation.auth.login

import androidx.annotation.StringRes

sealed class LoginSideEffect {
data object NavigateToHome : LoginSideEffect()
data class NavigateSignUp(val authId: String) : LoginSideEffect()
data class ShowToast(
@StringRes val message: Int,
val args: String? = null
) : LoginSideEffect()
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,94 @@
package com.sopt.presentation.auth.login

import android.content.Context
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.annotation.StringRes
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.identity.SignInClient
import com.google.android.gms.auth.api.identity.SignInCredential
import com.kakao.sdk.auth.model.OAuthToken
import com.kakao.sdk.common.model.ClientError
import com.kakao.sdk.common.model.ClientErrorCause
import com.kakao.sdk.user.UserApiClient
import com.sopt.core.util.BaseViewModel
import com.sopt.presentation.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Named

@HiltViewModel
class LoginViewModel @Inject constructor() : BaseViewModel<LoginSideEffect>() {
class LoginViewModel @Inject constructor(
@Named("GoogleClientId") private val googleClientId: String
) : BaseViewModel<LoginSideEffect>() {
private val _authId = MutableStateFlow("")
val authId: StateFlow<String> = _authId
private lateinit var oneTapClient: SignInClient

private val _authId = MutableStateFlow<String?>(null)
val authId: StateFlow<String?> = _authId
fun initializeGoogleSignIn(context: Context) {
oneTapClient = Identity.getSignInClient(context)
}

// Kakao Login
fun kakaoLogin(context: Context) {
val loginCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
handleError(error, R.string.toast_kakao_login_failed)
} else if (token != null) {
handleSuccess(token.accessToken, R.string.toast_kakao_login_success)
}
}

if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context, callback = loginCallback)
} else {
UserApiClient.instance.loginWithKakaoAccount(context, callback = loginCallback)
}
}

// Google Login
fun googleLogin(launcher: ActivityResultLauncher<IntentSenderRequest>) {
val signInRequest = com.google.android.gms.auth.api.identity.BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
com.google.android.gms.auth.api.identity.BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
.setServerClientId(googleClientId)
.setFilterByAuthorizedAccounts(false)
.build()
)
.build()

fun kakaoLogin() {
// TODO: 카카오 로그인
navigateToHome()
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener { result ->
launcher.launch(IntentSenderRequest.Builder(result.pendingIntent).build())
}
.addOnFailureListener { exception ->
handleError(exception, R.string.toast_google_login_failed)
}
}

fun googleLogin() {
// TODO: 구글 로그인
_authId.value = "google_access_token"
navigateToSignup(_authId.value.orEmpty())
fun handleGoogleLoginResult(credential: SignInCredential) {
if (!credential.googleIdToken.isNullOrEmpty()) {
handleSuccess(credential.googleIdToken.toString(), R.string.toast_google_login_success)
} else {
showToast(R.string.toast_google_login_failed)
}
}

private fun handleSuccess(authId: String, successMessageResId: Int) {
_authId.value = authId
showToast(successMessageResId)
navigateToSignup(authId)
}

private fun handleError(error: Throwable, @StringRes errorMessageResId: Int) {
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
showToast(R.string.toast_login_cancelled)
} else {
showToast(errorMessageResId, error.localizedMessage.orEmpty())
}
}

private fun navigateToSignup(authId: String) {
Expand All @@ -30,4 +98,13 @@ class LoginViewModel @Inject constructor() : BaseViewModel<LoginSideEffect>() {
private fun navigateToHome() {
emitSideEffect(LoginSideEffect.NavigateToHome)
}

private fun showToast(@StringRes messageResId: Int, vararg formatArgs: String) {
emitSideEffect(
LoginSideEffect.ShowToast(
message = messageResId,
args = formatArgs.joinToString(separator = ", ")
)
)
}
}
5 changes: 5 additions & 0 deletions presentation/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<string name="hint_signup_name">이름을 입력해주세요</string>
<string name="tv_signup_count">%1$d / %2$d</string>
<string name="toast_permission_gallery">갤러리 권한이 필요합니다.</string>
<string name="toast_kakao_login_success">카카오 로그인 성공</string>
<string name="toast_kakao_login_failed">카카오 로그인 실패: %s</string>
<string name="toast_login_cancelled">로그인이 취소되었습니다.</string>
<string name="toast_google_login_success">구글 로그인 성공</string>
<string name="toast_google_login_failed">구글 로그인 실패: %s</string>

<!-- 그룹 초대 입력 -->
<string name="iv_invite_description">그룹 초대 설명 이미지</string>
Expand Down
Loading