Skip to content

Commit

Permalink
Merge pull request #24 from Nexters/feature/kakao-login
Browse files Browse the repository at this point in the history
[FEAT] [#17] 카카오 소셜 로그인을 통한 엑세스 토큰 및 및 유저 정보 받아오는 파트 구현
  • Loading branch information
easyhooon authored Feb 2, 2024
2 parents 6fb565d + 279717f commit c58c2b2
Show file tree
Hide file tree
Showing 18 changed files with 220 additions and 15 deletions.
1 change: 1 addition & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
- name: Generate secrets.properties
run: |
echo "SERVER_BASE_URL=${{ secrets.SERVER_BASE_URL }}" >> secrets.properties
echo "KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> secrets.properties
- name: test Detekt
run: ./gradlew detekt
Expand Down
24 changes: 24 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.ilab.android.application)
alias(libs.plugins.ilab.android.application.compose)
alias(libs.plugins.ilab.android.hilt)
alias(libs.plugins.google.secrets)
}

android {
Expand All @@ -18,6 +19,24 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}

buildTypes {
getByName("debug") {
isDebuggable = true
applicationIdSuffix = ".dev"
manifestPlaceholders += mapOf(
"appName" to "@string/app_name_dev",
)
}

getByName("release") {
isDebuggable = false
signingConfig = signingConfigs.getByName("debug")
manifestPlaceholders += mapOf(
"appName" to "@string/app_name",
)
}
}
}

dependencies {
Expand All @@ -44,5 +63,10 @@ dependencies {
libs.androidx.splash,
libs.androidx.startup,
libs.timber,
libs.kakao.auth,
)
}

secrets {
defaultPropertiesFileName = "secrets.properties"
}
10 changes: 9 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

# kakao login
-keep class com.kakao.sdk.**.model.* { <fields>; }

# https://devtalk.kakao.com/t/method-authapi-issueaccesstoken/130860/4
# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
21 changes: 20 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:label="${appName}"
android:roundIcon="@mipmap/ic_launcher_round"
android:screenOrientation="portrait"
android:supportsRtl="true"
Expand All @@ -32,8 +32,27 @@
android:name="com.nexters.ilab.android.initializer.TimberInitializer"
android:value="androidx.startup" />

<meta-data
android:name="com.nexters.ilab.android.initializer.KakaoSDKInitializer"
android:value="androidx.startup" />

</provider>

<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<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${KAKAO_NATIVE_APP_KEY}" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.nexters.ilab.android.initializer

import android.content.Context
import androidx.startup.Initializer
import com.kakao.sdk.common.KakaoSdk
import com.nexters.ilab.android.BuildConfig

class KakaoSDKInitializer : Initializer<Unit> {

override fun create(context: Context) {
KakaoSdk.init(context, BuildConfig.KAKAO_NATIVE_APP_KEY)
}

override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<resources>
<string name="app_name">I\'Lab</string>
<string name="app_name_dev">I\'Lab.dev</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal fun Project.configureAndroid(extension: CommonExtension<*, *, *, *, *>)
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}
Expand Down
9 changes: 6 additions & 3 deletions core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<resources>
<string name="error_message_network">네트워크 연결이 원활하지 않습니다.</string>
<string name="error_message_unknown">알 수 없는 오류가 발생하였습니다.</string>

<!-- login-->
<string name="kakao_login">카카오 로그인</string>

<!-- main-->
<string name="home">홈</string>
<string name="upload_photo">사진 업로드</string>
<string name="my_page">마이페이지</string>
<string name="error_message_network">네트워크 연결이 원활하지 않습니다.</string>
<string name="error_message_unknown">알 수 없는 오류가 발생하였습니다.</string>

<!-- upload photo-->
<string name="upload_top_title">사진 가이드</string>
Expand Down Expand Up @@ -32,5 +36,4 @@
<string name="camera_permission_denied">"카메라 권한을 거부하였습니다. 앱 설정으로 이동하여 권한을 부여할 수 있습니다."</string>
<string name="select_photo">사진을 선택해 주세요.</string>


</resources>
1 change: 1 addition & 0 deletions feature/login/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ dependencies {
implementations(
libs.androidx.core,
libs.timber,
libs.kakao.auth,
)
}
25 changes: 25 additions & 0 deletions feature/login/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

# https://stackoverflow.com/questions/70037537/proguard-missing-classes-detected-while-running-r8-after-adding-package-names-in
-dontwarn java.lang.invoke.StringConcatFactory

Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ class LoginActivity : ComponentActivity() {
setContent {
ILabTheme {
LoginRoute(
onLoginClick = {
navigateToHome = {
mainNavigator.navigateFrom(
activity = this,
withFinish = true,
)
},
onShowErrorSnackBar = {},
viewModel = viewModel,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,96 @@
package com.nexters.ilab.android.feature.login

import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kakao.sdk.auth.model.OAuthToken
import com.kakao.sdk.common.model.AuthError
import com.kakao.sdk.user.UserApiClient
import com.nexters.ilab.android.core.common.UiText
import com.nexters.ilab.android.core.designsystem.R
import timber.log.Timber

@Suppress("unused")
@Composable
internal fun LoginRoute(
onLoginClick: () -> Unit,
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
navigateToHome: () -> Unit,
viewModel: LoginViewModel = hiltViewModel(),
) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current

val kakaoCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
when {
error != null -> when {
(error is AuthError && error.response.error == "ProtocolError") -> {
Timber.e("로그인 실패: ${error.response.error}, ${error.response.errorDescription}")
viewModel.setErrorMessage(UiText.StringResource(R.string.error_message_network))
}

else -> {
Timber.e("로그인 실패: ${error.message}")
viewModel.setErrorMessage(UiText.StringResource(R.string.error_message_unknown))
}
}

token != null -> UserApiClient.instance.me { user, _ ->
user?.let {
Timber.d("로그인 성공: ${token.accessToken}, ${it.kakaoAccount?.profile?.nickname}, ${it.kakaoAccount?.profile?.profileImageUrl}")
viewModel.kakaoLogin()
} ?: viewModel.setErrorMessage(UiText.StringResource(R.string.error_message_unknown))
}

else -> viewModel.setErrorMessage(UiText.StringResource(R.string.error_message_unknown))
}
}

LaunchedEffect(viewModel) {
viewModel.container.sideEffectFlow.collect { sideEffect ->
when (sideEffect) {
is LoginSideEffect.KakaoLogin -> {
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context, callback = kakaoCallback)
} else {
UserApiClient.instance.loginWithKakaoAccount(context, callback = kakaoCallback)
}
}

is LoginSideEffect.LoginSuccess -> navigateToHome()
is LoginSideEffect.ShowToast -> Toast.makeText(context, sideEffect.message.asString(context), Toast.LENGTH_SHORT).show()
}
}
}

LoginScreen(
onLoginClick = onLoginClick,
uiState = uiState,
onLoginClick = viewModel::onLoginButtonClick,
)
}

@Composable
internal fun LoginScreen(
uiState: LoginState,
onLoginClick: () -> Unit,
) {
if (uiState.isLoading) {
CircularProgressIndicator()
}

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
Expand All @@ -39,7 +101,7 @@ internal fun LoginScreen(
Button(
onClick = onLoginClick,
) {
Text(text = "Login")
Text(text = stringResource(id = R.string.kakao_login))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nexters.ilab.android.feature.login

import com.nexters.ilab.android.core.common.UiText

sealed interface LoginSideEffect {
data object KakaoLogin : LoginSideEffect
data object LoginSuccess : LoginSideEffect
data class ShowToast(val message: UiText) : LoginSideEffect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.nexters.ilab.android.feature.login

data class LoginState(
val isLoading: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
package com.nexters.ilab.android.feature.login

import androidx.lifecycle.ViewModel
import com.nexters.ilab.android.core.common.UiText
import dagger.hilt.android.lifecycle.HiltViewModel
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import javax.inject.Inject

@HiltViewModel
class LoginViewModel @Inject constructor() : ViewModel()
class LoginViewModel @Inject constructor() : ViewModel(), ContainerHost<LoginState, LoginSideEffect> {

override val container = container<LoginState, LoginSideEffect>(LoginState())

fun onLoginButtonClick() = intent {
postSideEffect(LoginSideEffect.KakaoLogin)
}

fun kakaoLogin() = intent {
reduce {
state.copy(isLoading = true)
}
// TODO repository function call
reduce {
state.copy(isLoading = false)
}
postSideEffect(LoginSideEffect.LoginSuccess)
}

fun setErrorMessage(message: UiText) = intent {
postSideEffect(LoginSideEffect.ShowToast(message))
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ firebase-crashlytics = "2.9.9"
ksp = "1.9.22-1.0.16"
orbit-core = "4.6.1"
kotest = "5.8.0"
kakao-core = "2.19.0"

[libraries]

Expand Down Expand Up @@ -94,6 +95,7 @@ orbit-viewmodel = { group = "org.orbit-mvi", name = "orbit-viewmodel", version.r
orbit-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbit-core" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-compose" }
compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version.ref = "compose-shimmer" }
kakao-auth = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-core" }

kotest-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest" }
kotest-assertion = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" }
Expand Down
Loading

0 comments on commit c58c2b2

Please sign in to comment.