diff --git a/.github/workflows/android-pull-request-ci.yml b/.github/workflows/android-pull-request-ci.yml index bc6da2c7..c9bccc6a 100644 --- a/.github/workflows/android-pull-request-ci.yml +++ b/.github/workflows/android-pull-request-ci.yml @@ -22,8 +22,10 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Create local.properties with base.url - run: echo "base.url=https://ci-placeholder.local" >> local.properties + - name: Create local.properties with base.url and kakao.native.key + run: | + echo "base.url=https://ci-placeholder.local" >> local.properties + echo "kakao.native.key=${{ secrets.KAKAO_NATIVE_KEY }}" >> local.properties - name: Run ktlint run: ./gradlew ktlintCheck \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 243cc8bf..c6c1b976 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,4 +21,6 @@ dependencies { implementation(projects.remote) implementation(projects.domain) implementation(projects.feature.main) + + implementation(libs.kakao.login) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f164c8a9..4f824c48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,5 +17,19 @@ android:usesCleartextTraffic="true" tools:targetApi="31"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/teamsolply/solply/SolplyApplication.kt b/app/src/main/java/com/teamsolply/solply/SolplyApplication.kt index 37ac4a92..1241e236 100644 --- a/app/src/main/java/com/teamsolply/solply/SolplyApplication.kt +++ b/app/src/main/java/com/teamsolply/solply/SolplyApplication.kt @@ -1,11 +1,15 @@ package com.teamsolply.solply import android.app.Application +import com.kakao.sdk.common.KakaoSdk +import com.teamsolply.solply.buildconfig.BuildConfig.KAKAO_NATIVE_KEY import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class SolplyApplication : Application() { override fun onCreate() { super.onCreate() + + KakaoSdk.init(this, KAKAO_NATIVE_KEY) } } diff --git a/build-logic/convention/src/main/java/AndroidApplicationPlugin.kt b/build-logic/convention/src/main/java/AndroidApplicationPlugin.kt index ec23dabd..d2fce12c 100644 --- a/build-logic/convention/src/main/java/AndroidApplicationPlugin.kt +++ b/build-logic/convention/src/main/java/AndroidApplicationPlugin.kt @@ -1,4 +1,5 @@ import com.android.build.api.dsl.ApplicationExtension +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.teamsolply.solply.convention.configureAndroidCompose import com.teamsolply.solply.convention.configureKotlinAndroid import com.teamsolply.solply.convention.extension.getLibrary @@ -24,6 +25,7 @@ internal class AndroidApplicationPlugin : Plugin { targetSdk = libs.getVersion("targetSdk").requiredVersion.toInt() versionCode = libs.getVersion("versionCode").requiredVersion.toInt() versionName = libs.getVersion("versionName").requiredVersion + manifestPlaceholders["KAKAO_NATIVE_KEY"] = gradleLocalProperties(rootDir, providers).getProperty("kakao.native.key").replace("\"", "") } } diff --git a/build-logic/convention/src/main/java/com/teamsolply/solply/convention/BuildConfig.kt b/build-logic/convention/src/main/java/com/teamsolply/solply/convention/BuildConfig.kt index d62eddd9..50c5a73d 100644 --- a/build-logic/convention/src/main/java/com/teamsolply/solply/convention/BuildConfig.kt +++ b/build-logic/convention/src/main/java/com/teamsolply/solply/convention/BuildConfig.kt @@ -14,6 +14,11 @@ internal fun Project.configureBuildConfig( "BASE_URL", gradleLocalProperties(rootDir, providers).getProperty("base.url") ) + buildConfigField( + "String", + "KAKAO_NATIVE_KEY", + gradleLocalProperties(rootDir, providers).getProperty("kakao.native.key") + ) } buildFeatures { diff --git a/core/buildconfig/src/main/java/com/teamsolply/solply/buildconfig/impl/BuildConfigFieldsProviderImpl.kt b/core/buildconfig/src/main/java/com/teamsolply/solply/buildconfig/impl/BuildConfigFieldsProviderImpl.kt index 7b8134b7..79f45e18 100644 --- a/core/buildconfig/src/main/java/com/teamsolply/solply/buildconfig/impl/BuildConfigFieldsProviderImpl.kt +++ b/core/buildconfig/src/main/java/com/teamsolply/solply/buildconfig/impl/BuildConfigFieldsProviderImpl.kt @@ -1,6 +1,7 @@ package com.teamsolply.solply.buildconfig.impl -import com.teamsolply.solply.buildconfig.BuildConfig +import com.teamsolply.solply.buildconfig.BuildConfig.BASE_URL +import com.teamsolply.solply.buildconfig.BuildConfig.KAKAO_NATIVE_KEY import com.teamsolply.solply.common.buildconfig.BuildConfigFieldProvider import com.teamsolply.solply.common.buildconfig.BuildConfigFields import javax.inject.Inject @@ -8,7 +9,8 @@ import javax.inject.Inject class BuildConfigFieldsProviderImpl @Inject constructor() : BuildConfigFieldProvider { override fun get(): BuildConfigFields = BuildConfigFields( - baseUrl = BuildConfig.BASE_URL, + baseUrl = BASE_URL, + kakaoNativeKey = KAKAO_NATIVE_KEY, isDebug = true ) } diff --git a/core/common/src/main/java/com/teamsolply/solply/common/buildconfig/BuildConfigFields.kt b/core/common/src/main/java/com/teamsolply/solply/common/buildconfig/BuildConfigFields.kt index 7ee2225d..4676c9ee 100644 --- a/core/common/src/main/java/com/teamsolply/solply/common/buildconfig/BuildConfigFields.kt +++ b/core/common/src/main/java/com/teamsolply/solply/common/buildconfig/BuildConfigFields.kt @@ -2,5 +2,6 @@ package com.teamsolply.solply.common.buildconfig data class BuildConfigFields( val baseUrl: String, + val kakaoNativeKey: String, val isDebug: Boolean ) diff --git a/core/ui/src/main/java/com/teamsolply/solply/ui/lifecycle/LaunchedEffectWithLifecycle.kt b/core/ui/src/main/java/com/teamsolply/solply/ui/lifecycle/LaunchedEffectWithLifecycle.kt new file mode 100644 index 00000000..4d37a187 --- /dev/null +++ b/core/ui/src/main/java/com/teamsolply/solply/ui/lifecycle/LaunchedEffectWithLifecycle.kt @@ -0,0 +1,24 @@ +package com.teamsolply.solply.ui.lifecycle + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope + +@Composable +fun LaunchedEffectWithLifecycle( + key1: Any? = Unit, + key2: Any? = Unit, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + action: suspend CoroutineScope.() -> Unit = {} +) { + LaunchedEffect(key1 = key1, key2 = key2) { + lifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) { + action() + } + } +} diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 668e0f65..e420306b 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -7,5 +7,6 @@ android { } dependencies { + implementation(projects.feature.oauth) implementation(projects.feature.home) } diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt index 9e18cdd9..7a81d09f 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt @@ -9,8 +9,8 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import com.teamsolply.solply.home.navigation.Home import com.teamsolply.solply.home.navigation.navigateHome +import com.teamsolply.solply.oauth.navigation.Oauth internal class MainNavigator( val navController: NavHostController @@ -19,7 +19,7 @@ internal class MainNavigator( @Composable get() = navController .currentBackStackEntryAsState().value?.destination - val startDestination = Home + val startDestination = Oauth val currentTab: MainNavTab? @Composable get() = MainNavTab.find { tab -> diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt index 24391ed9..ab50be6b 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt @@ -14,6 +14,7 @@ import androidx.navigation.compose.NavHost import com.teamsolply.solply.designsystem.theme.SolplyTheme import com.teamsolply.solply.home.navigation.homeNavGraph import com.teamsolply.solply.main.component.MainBottomBar +import com.teamsolply.solply.oauth.navigation.oauthNavGraph import kotlinx.collections.immutable.toPersistentList @Composable @@ -33,6 +34,7 @@ internal fun MainScreen( .background(color = SolplyTheme.colors.white) .fillMaxSize() ) { + oauthNavGraph(paddingValues = innerPadding) homeNavGraph(paddingValues = innerPadding) } }, diff --git a/feature/oauth/.gitignore b/feature/oauth/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/oauth/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/oauth/build.gradle.kts b/feature/oauth/build.gradle.kts new file mode 100644 index 00000000..75fcbced --- /dev/null +++ b/feature/oauth/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.solply.feature) +} + +android { + namespace = "com.teamsolply.solply.oauth" +} + +dependencies { + implementation(libs.kakao.login) +} diff --git a/feature/oauth/src/main/AndroidManifest.xml b/feature/oauth/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/oauth/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/oauth/src/main/java/com/teamsolply/solply/oauth/navigation/OauthNavigation.kt b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/navigation/OauthNavigation.kt new file mode 100644 index 00000000..2ad000fc --- /dev/null +++ b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/navigation/OauthNavigation.kt @@ -0,0 +1,29 @@ +package com.teamsolply.solply.oauth.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.teamsolply.solply.navigation.Route +import com.teamsolply.solply.oauth.presentation.OauthRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateOauth( + navOptions: NavOptions +) { + navigate(Oauth, navOptions) +} + +fun NavGraphBuilder.oauthNavGraph( + paddingValues: PaddingValues +) { + composable { + OauthRoute( + paddingValues = paddingValues + ) + } +} + +@Serializable +data object Oauth : Route diff --git a/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthContract.kt b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthContract.kt new file mode 100644 index 00000000..028732d9 --- /dev/null +++ b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthContract.kt @@ -0,0 +1,17 @@ +package com.teamsolply.solply.oauth.presentation + +import com.teamsolply.solply.ui.base.SideEffect +import com.teamsolply.solply.ui.base.UiIntent +import com.teamsolply.solply.ui.base.UiState + +data class OauthState( + val g: String = "" +) : UiState + +sealed interface OauthIntent : UiIntent { + data object KakaoLoginClick : OauthIntent +} + +sealed interface OauthSideEffect : SideEffect { + data object StartKakaoLogin : OauthSideEffect +} diff --git a/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthScreen.kt b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthScreen.kt new file mode 100644 index 00000000..4e3fae43 --- /dev/null +++ b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthScreen.kt @@ -0,0 +1,123 @@ +package com.teamsolply.solply.oauth.presentation + +import android.app.Activity +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.user.UserApiClient +import com.teamsolply.solply.ui.extension.customClickable +import com.teamsolply.solply.ui.lifecycle.LaunchedEffectWithLifecycle +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun OauthRoute( + paddingValues: PaddingValues, + viewModel: OauthViewModel = hiltViewModel() +) { + val context = LocalContext.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffectWithLifecycle { + viewModel.sideEffect.collectLatest { sideEffect -> + when (sideEffect) { + OauthSideEffect.StartKakaoLogin -> startKakaoLogin( + context = context, + onSuccess = { accessToken, refreshToken -> + Toast.makeText(context, "로그인 성공", Toast.LENGTH_SHORT).show() + Log.d( + "asdasdasd", + "accessToken: ${accessToken}\n refreshToken: $refreshToken" + ) + }, + onFailure = { error -> + Log.d("asdasdasd", error.toString()) + } + ) + } + } + } + + OauthScreen( + kakaoLoginClick = { viewModel.sendIntent(OauthIntent.KakaoLoginClick) } + ) +} + +@Composable +fun OauthScreen( + kakaoLoginClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Oauth", + modifier = Modifier.customClickable( + onClick = kakaoLoginClick + ) + ) + } +} + +fun startKakaoLogin( + context: Context, + onSuccess: (accessToken: String, refreshToken: String?) -> Unit = { _, _ -> }, + onFailure: (Throwable) -> Unit = {} +) { + val activity = context as? Activity ?: return + + if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { + UserApiClient.instance.loginWithKakaoTalk(activity) { token, error -> + if (error != null) { + if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { + Toast.makeText(context, "로그인 취소", Toast.LENGTH_SHORT).show() + return@loginWithKakaoTalk + } + UserApiClient.instance.loginWithKakaoAccount(context) { accountToken, accountError -> + if (accountError != null) { + onFailure(accountError) + Toast.makeText( + context, + "로그인 실패: ${accountError.message}", + Toast.LENGTH_SHORT + ).show() + return@loginWithKakaoAccount + } + if (accountToken != null) { + onSuccess(accountToken.accessToken, accountToken.refreshToken) + } + } + } else if (token != null) { + onSuccess(token.accessToken, token.refreshToken) + } + } + } else { + UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> + Log.d("KAKAO_LOGIN", "loginWithKakaoAccount called. token=$token, error=$error") + if (error != null) { + onFailure(error) + Toast.makeText(context, "로그인 실패: ${error.message}", Toast.LENGTH_SHORT).show() + return@loginWithKakaoAccount + } + if (token != null) { + onSuccess(token.accessToken, token.refreshToken) + } + } + } +} diff --git a/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthViewModel.kt b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthViewModel.kt new file mode 100644 index 00000000..993de13f --- /dev/null +++ b/feature/oauth/src/main/java/com/teamsolply/solply/oauth/presentation/OauthViewModel.kt @@ -0,0 +1,15 @@ +package com.teamsolply.solply.oauth.presentation + +import com.teamsolply.solply.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class OauthViewModel @Inject constructor() : + BaseViewModel(OauthState()) { + override fun handleIntent(intent: OauthIntent) { + when (intent) { + OauthIntent.KakaoLoginClick -> postSideEffect(OauthSideEffect.StartKakaoLogin) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ead2ff9f..6b97480d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") } } @@ -37,3 +38,4 @@ include(":local") include(":remote") include(":feature:main") include(":feature:home") +include(":feature:oauth")