diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7633f067..a211eae0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(project(":core:design-system")) implementation(project(":core:ui")) implementation(project(":core:model")) + implementation(project(":core:common")) implementation(project(":feature:login")) implementation(project(":feature:main")) @@ -27,5 +28,7 @@ dependencies { implementation(project(":feature:score-mission")) implementation(project(":feature:shop")) implementation(libs.junit) + implementation(libs.google.services) + implementation(libs.play.services.auth) androidTestImplementation(libs.androidx.test.ext) } \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 00000000..6219a2d1 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "322299804217", + "project_id": "stack-knowledge-v2", + "storage_bucket": "stack-knowledge-v2.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:322299804217:android:e15d7c6ad9b47cc65e2be8", + "android_client_info": { + "package_name": "com.kdn.stack_knowledge" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBxMjD7Mg9vzfBiuZVBzyR12Ubr-uEZ_hQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ee0d221..693f6fc3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ + @@ -26,6 +27,7 @@ + - \ No newline at end of file + diff --git a/app/src/main/java/com/kdn/stack_knowledge/MainActivity.kt b/app/src/main/java/com/kdn/stack_knowledge/MainActivity.kt new file mode 100644 index 00000000..7633a287 --- /dev/null +++ b/app/src/main/java/com/kdn/stack_knowledge/MainActivity.kt @@ -0,0 +1,121 @@ +package com.kdn.stack_knowledge + +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.CompositionLocalProvider +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import com.example.common.toast.makeToast +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.Scope +import com.google.android.gms.tasks.Task +import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme +import com.stackknowledge.login.viewmodel.AuthViewModel +import com.kdn.stack_knowledge.ui.StackKnowledgeApp +import com.stackknowledge.user.R +import dagger.hilt.android.AndroidEntryPoint +import remote.request.auth.LoginRequestModel +import javax.inject.Inject +import javax.inject.Named + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + @Inject + @Named("GOOGLE_CLIENT_ID") + lateinit var googleClientId: String + + @Inject + @Named("SCOPE") + lateinit var scope: String + + private val viewModel by viewModels() + + private val googleSignInClient: GoogleSignInClient by lazy { getGoogleClient() } + + private val googleSignInLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val task: Task = + GoogleSignIn.getSignedInAccountFromIntent(result.data) + handleGoogleSignInResult(task) + } + + private var doubleBackToExitPressedOnce = false + + private var backPressedTimestamp = 0L + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + controlTheStackWhenBackPressed() + } + } + + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + setContent { + CompositionLocalProvider(LocalViewModelStoreOwner provides this) { + StackKnowledgeAndroidTheme { _, _ -> + StackKnowledgeApp( + windowSizeClass = calculateWindowSizeClass(this@MainActivity), + onLoginButtonClick = { + googleSocialLogin() + } + ) + } + } + } + } + + private fun controlTheStackWhenBackPressed() { + val currentTime = System.currentTimeMillis() + if (doubleBackToExitPressedOnce && currentTime - backPressedTimestamp <= 2000) { + finishAffinity() + } else { + doubleBackToExitPressedOnce = true + backPressedTimestamp = currentTime + makeToast(this, getString(R.string.close_app), Toast.LENGTH_SHORT) + } + } + + private fun googleSocialLogin() { + googleSignInClient.signOut().addOnCompleteListener { + val signInIntent = googleSignInClient.signInIntent + googleSignInLauncher.launch(signInIntent) + } + } + + private fun getGoogleClient(): GoogleSignInClient { + val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(scope)) + .requestServerAuthCode(googleClientId) + .requestEmail() + .build() + + return GoogleSignIn.getClient(this@MainActivity, googleSignInOptions) + } + + private fun handleGoogleSignInResult(task: Task) { + val account = task.getResult(ApiException::class.java) + + with(viewModel) { + if (isStudent.value) { + loginStudent(body = LoginRequestModel(code = account.serverAuthCode.toString())) + } else { + loginTeacher(body = LoginRequestModel(code = account.serverAuthCode.toString())) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stackknowledge/StackKnowledgeApplication.kt b/app/src/main/java/com/kdn/stack_knowledge/StackKnowledgeApplication.kt similarity index 53% rename from app/src/main/java/com/stackknowledge/StackKnowledgeApplication.kt rename to app/src/main/java/com/kdn/stack_knowledge/StackKnowledgeApplication.kt index a5af556c..21f9b8e6 100644 --- a/app/src/main/java/com/stackknowledge/StackKnowledgeApplication.kt +++ b/app/src/main/java/com/kdn/stack_knowledge/StackKnowledgeApplication.kt @@ -1,7 +1,7 @@ -package com.stackknowledge +package com.kdn.stack_knowledge import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class StackKnowledgeApplication : Application() +class StackKnowledgeApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/com/stackknowledge/navigation/StackKnowledgeNavHost.kt b/app/src/main/java/com/kdn/stack_knowledge/navigation/StackKnowledgeNavHost.kt similarity index 90% rename from app/src/main/java/com/stackknowledge/navigation/StackKnowledgeNavHost.kt rename to app/src/main/java/com/kdn/stack_knowledge/navigation/StackKnowledgeNavHost.kt index 8685bd7f..85ef2c28 100644 --- a/app/src/main/java/com/stackknowledge/navigation/StackKnowledgeNavHost.kt +++ b/app/src/main/java/com/kdn/stack_knowledge/navigation/StackKnowledgeNavHost.kt @@ -1,4 +1,4 @@ -package com.stackknowledge.navigation +package com.kdn.stack_knowledge.navigation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -8,10 +8,9 @@ import com.stackknowledge.login.navigation.loginScreen import com.stackknowledge.login.navigation.navigateToLogin import com.stackknowledge.login.navigation.roleCheckRoute import com.stackknowledge.login.navigation.roleCheckScreen -import com.stackknowledge.main.navigation.mainPageRoute import com.stackknowledge.main.navigation.mainScreen import com.stackknowledge.main.navigation.navigateToMain -import com.stackknowledge.navigation.util.bottomNavigationNavigate +import com.kdn.stack_knowledge.navigation.util.bottomNavigationNavigate import com.stackknowledge.ranking.navigation.navigateToRanking import com.stackknowledge.ranking.navigation.navigateToTeacherRanking import com.stackknowledge.ranking.navigation.rankingScreen @@ -24,21 +23,20 @@ import com.stackknowledge.shop.navigation.shopRoute import com.stackknowledge.shop.navigation.shopScreen import com.stackknowledge.shop.navigation.teacherShopRoute import com.stackknowledge.shop.navigation.teacherShopScreen -import com.stackknowledge.ui.StackKnowledgeAppState -import com.stackkowledge.mission.navigation.createMissionRoute +import com.kdn.stack_knowledge.ui.StackKnowledgeAppState import com.stackkowledge.mission.navigation.createMissionScreen -import com.stackkowledge.mission.navigation.entireMissionRoute import com.stackkowledge.mission.navigation.entireMissionScreen import com.stackkowledge.mission.navigation.navigateToEntireMission import com.stackkowledge.mission.navigation.navigateToResolveMission import com.stackkowledge.mission.navigation.resolveMissionScreen -import enumdatatype.Authority +import enumdata.Authority @Composable fun StackKnowledgeNavHost( appState: StackKnowledgeAppState, - modifier: Modifier = Modifier, startDestination: String = roleCheckRoute, + modifier: Modifier = Modifier, + onLoginButtonClick: () -> Unit = {}, ) { val navController = appState.navController @@ -47,9 +45,12 @@ fun StackKnowledgeNavHost( startDestination = startDestination, modifier = modifier ) { - loginScreen() + loginScreen( + onSuccess = navController::navigateToMain, + onLoginButtonClick = onLoginButtonClick + ) roleCheckScreen( - onRoleClick = navController::navigateToLogin + onRoleButtonClick = navController::navigateToLogin ) mainScreen( onNavigate = { role, navType, index -> diff --git a/app/src/main/java/com/kdn/stack_knowledge/navigation/TopLevelDestination.kt b/app/src/main/java/com/kdn/stack_knowledge/navigation/TopLevelDestination.kt new file mode 100644 index 00000000..03b87dc0 --- /dev/null +++ b/app/src/main/java/com/kdn/stack_knowledge/navigation/TopLevelDestination.kt @@ -0,0 +1,5 @@ +package com.kdn.stack_knowledge.navigation + +enum class TopLevelDestination { + ROLE_CHECK, +} \ No newline at end of file diff --git a/app/src/main/java/com/stackknowledge/navigation/util/BottomNavigationNavigate.kt b/app/src/main/java/com/kdn/stack_knowledge/navigation/util/BottomNavigationNavigate.kt similarity index 96% rename from app/src/main/java/com/stackknowledge/navigation/util/BottomNavigationNavigate.kt rename to app/src/main/java/com/kdn/stack_knowledge/navigation/util/BottomNavigationNavigate.kt index 77adbbf6..cf47d712 100644 --- a/app/src/main/java/com/stackknowledge/navigation/util/BottomNavigationNavigate.kt +++ b/app/src/main/java/com/kdn/stack_knowledge/navigation/util/BottomNavigationNavigate.kt @@ -1,4 +1,4 @@ -package com.stackknowledge.navigation.util +package com.kdn.stack_knowledge.navigation.util import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination @@ -12,7 +12,7 @@ import com.stackknowledge.shop.navigation.navigateToShop import com.stackknowledge.shop.navigation.navigateToTeacherShop import com.stackkowledge.mission.navigation.navigateToCreateMission import com.stackkowledge.mission.navigation.navigateToEntireMission -import enumdatatype.Authority +import enumdata.Authority fun bottomNavigationNavigate( role: Authority, diff --git a/app/src/main/java/com/stackknowledge/ui/StackKnowledgeApp.kt b/app/src/main/java/com/kdn/stack_knowledge/ui/StackKnowledgeApp.kt similarity index 61% rename from app/src/main/java/com/stackknowledge/ui/StackKnowledgeApp.kt rename to app/src/main/java/com/kdn/stack_knowledge/ui/StackKnowledgeApp.kt index d9497105..ee988f04 100644 --- a/app/src/main/java/com/stackknowledge/ui/StackKnowledgeApp.kt +++ b/app/src/main/java/com/kdn/stack_knowledge/ui/StackKnowledgeApp.kt @@ -1,9 +1,9 @@ -package com.stackknowledge.ui +package com.kdn.stack_knowledge.ui import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme -import com.stackknowledge.navigation.StackKnowledgeNavHost +import com.kdn.stack_knowledge.navigation.StackKnowledgeNavHost @Composable fun StackKnowledgeApp( @@ -11,11 +11,12 @@ fun StackKnowledgeApp( appState: StackKnowledgeAppState = rememberStackKnowledgeAppState( windowSizeClass = windowSizeClass ), + onLoginButtonClick: () -> Unit = {}, ) { StackKnowledgeAndroidTheme { _, _ -> - StackKnowledgeNavHost( - appState = appState, - //startDestination = "" <- auth 작업후에 추가 - ) + StackKnowledgeNavHost( + appState = appState, + onLoginButtonClick = onLoginButtonClick + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/stackknowledge/ui/StackKnowledgeAppState.kt b/app/src/main/java/com/kdn/stack_knowledge/ui/StackKnowledgeAppState.kt similarity index 89% rename from app/src/main/java/com/stackknowledge/ui/StackKnowledgeAppState.kt rename to app/src/main/java/com/kdn/stack_knowledge/ui/StackKnowledgeAppState.kt index 6c2f57d2..59fe2488 100644 --- a/app/src/main/java/com/stackknowledge/ui/StackKnowledgeAppState.kt +++ b/app/src/main/java/com/kdn/stack_knowledge/ui/StackKnowledgeAppState.kt @@ -1,4 +1,4 @@ -package com.stackknowledge.ui +package com.kdn.stack_knowledge.ui import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass @@ -6,12 +6,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.stackknowledge.navigation.TopLevelDestination +import com.kdn.stack_knowledge.navigation.TopLevelDestination +import com.stackknowledge.login.navigation.roleCheckRoute import kotlinx.coroutines.CoroutineScope @Composable @@ -45,7 +45,7 @@ class StackKnowledgeAppState( val currentTopLevelDestination: TopLevelDestination? @Composable get() = when(currentDestination?.route) { - // loginRoute 작성 + roleCheckRoute -> TopLevelDestination.ROLE_CHECK else -> null } diff --git a/app/src/main/java/com/stackknowledge/MainActivity.kt b/app/src/main/java/com/stackknowledge/MainActivity.kt deleted file mode 100644 index dd3c51b3..00000000 --- a/app/src/main/java/com/stackknowledge/MainActivity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.stackknowledge - -import android.os.Bundle -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.OnBackPressedCallback -import androidx.activity.compose.setContent -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.CompositionLocalProvider -import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme -import com.stackknowledge.ui.StackKnowledgeApp -import com.stackknowledge.user.R -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MainActivity : ComponentActivity() { - private var doubleBackToExitPressedOnce = false - private var backPressedTimestamp = 0L - private val onBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - controlTheStackWhenBackPressed() - } - } - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - this.onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - setContent { - CompositionLocalProvider { - StackKnowledgeAndroidTheme { _, _ -> - StackKnowledgeApp(windowSizeClass = calculateWindowSizeClass(this@MainActivity)) - } - } - } - } - private fun controlTheStackWhenBackPressed() { - val currentTime = System.currentTimeMillis() - if (doubleBackToExitPressedOnce && currentTime - backPressedTimestamp <= 2000) { - finishAffinity() - } else { - doubleBackToExitPressedOnce = true - backPressedTimestamp = currentTime - Toast.makeText(this, getString(R.string.close_app),Toast.LENGTH_SHORT).show() - } - } -} diff --git a/app/src/main/java/com/stackknowledge/navigation/TopLevelDestination.kt b/app/src/main/java/com/stackknowledge/navigation/TopLevelDestination.kt deleted file mode 100644 index 174d6cae..00000000 --- a/app/src/main/java/com/stackknowledge/navigation/TopLevelDestination.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.stackknowledge.navigation - -enum class TopLevelDestination { - LOGIN, -} \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/convention/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/java/com/convention/AndroidApplicationConventionPlugin.kt index 93e799d1..ae4e535b 100644 --- a/build-logic/convention/src/main/java/com/convention/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/java/com/convention/AndroidApplicationConventionPlugin.kt @@ -8,7 +8,6 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies - class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { @@ -20,7 +19,7 @@ class AndroidApplicationConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) defaultConfig { - applicationId = "com.stackknowledge_android" + applicationId = "com.kdn.stack_knowledge" minSdk = 26 targetSdk = 34 versionCode = 1 @@ -48,8 +47,6 @@ class AndroidApplicationConventionPlugin : Plugin { add("implementation", libs.findBundle("compose").get()) } } - } - } } \ No newline at end of file diff --git a/core/common/consumer-rules.pro b/core/common/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/common/proguard-rules.pro b/core/common/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/common/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index b957e4a5..d54c565f 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -16,4 +16,5 @@ dependencies { implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) implementation(libs.retrofit.moshi.converter) + implementation(libs.retrofit.moshi.codegen) } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/stackknowledge/di/RepositoryModule.kt b/core/data/src/main/kotlin/com/stackknowledge/di/RepositoryModule.kt index d44e5622..fcd84876 100644 --- a/core/data/src/main/kotlin/com/stackknowledge/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/com/stackknowledge/di/RepositoryModule.kt @@ -1,5 +1,7 @@ package com.stackknowledge.di +import com.stackknowledge.repository.auth.AuthRepository +import com.stackknowledge.repository.auth.AuthRepositoryImpl import com.stackknowledge.repository.item.ItemRepository import com.stackknowledge.repository.item.ItemRepositoryImpl import com.stackknowledge.repository.mission.MissionRepository @@ -20,6 +22,11 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { + @Binds + abstract fun bindAuthRepository( + missionRepositoryImpl: AuthRepositoryImpl + ): AuthRepository + @Binds abstract fun bindMissionRepository( missionRepositoryImpl: MissionRepositoryImpl diff --git a/core/data/src/main/kotlin/com/stackknowledge/repository/auth/AuthRepository.kt b/core/data/src/main/kotlin/com/stackknowledge/repository/auth/AuthRepository.kt new file mode 100644 index 00000000..16cbf5fc --- /dev/null +++ b/core/data/src/main/kotlin/com/stackknowledge/repository/auth/AuthRepository.kt @@ -0,0 +1,15 @@ +package com.stackknowledge.repository.auth + +import kotlinx.coroutines.flow.Flow +import remote.request.auth.LoginRequestModel +import remote.response.auth.LoginResponseModel + +interface AuthRepository { + fun loginStudent(body: LoginRequestModel): Flow + + fun loginTeacher(body: LoginRequestModel): Flow + + suspend fun saveToken(token: LoginResponseModel) + + fun logout(): Flow +} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/stackknowledge/repository/auth/AuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/stackknowledge/repository/auth/AuthRepositoryImpl.kt new file mode 100644 index 00000000..f599741e --- /dev/null +++ b/core/data/src/main/kotlin/com/stackknowledge/repository/auth/AuthRepositoryImpl.kt @@ -0,0 +1,38 @@ +package com.stackknowledge.repository.auth + +import com.stackknowledge.datasource.auth.AuthDataSource +import com.stackknowledge.datastore.LocalAuthDataSource +import com.stackknowledge.mapper.request.auth.toDto +import com.stackknowledge.mapper.response.auth.toModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import remote.request.auth.LoginRequestModel +import remote.response.auth.LoginResponseModel +import javax.inject.Inject + +class AuthRepositoryImpl @Inject constructor( + private val authDataSource: AuthDataSource, + private val localDataSource: LocalAuthDataSource +): AuthRepository { + override fun loginStudent(body: LoginRequestModel): Flow { + return authDataSource.loginStudent(body = body.toDto()).map { it.toModel() } + } + + override fun loginTeacher(body: LoginRequestModel): Flow { + return authDataSource.loginTeacher(body = body.toDto()).map { it.toModel() } + } + + override suspend fun saveToken(token: LoginResponseModel) { + token.let { + localDataSource.setAccessToken(it.accessToken) + localDataSource.setAccessTime(it.expiredAt) + localDataSource.setRefreshToken(it.refreshToken) + localDataSource.setRefreshTime(it.expiredAt) + localDataSource.setAuthorityInfo(it.authority.toString()) + } + } + + override fun logout(): Flow { + return authDataSource.logout() + } +} \ No newline at end of file diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index bb63c8e6..56e258cc 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -2,34 +2,15 @@ plugins { id("stackknowledge.android.core") id("stackknowledge.android.hilt") - alias(libs.plugins.protobuf) } android { namespace = "com.stackknowledge.datastore" } -protobuf { - protoc { - artifact = libs.protobuf.protoc.get().toString() - } - generateProtoTasks { - all().forEach { task -> - task.builtins { - register("java") { - option("lite") - } - register("kotlin") { - option("lite") - } - } - } - } -} - dependencies { implementation(project(":core:model")) implementation(libs.androidx.dataStore.core) - implementation(libs.protobuf.kotlin.lite) + implementation(libs.androidx.dataStore.preferences) } \ No newline at end of file diff --git a/core/datastore/src/main/java/com/stackknowledge/datastore/LocalAuthDataSource.kt b/core/datastore/src/main/java/com/stackknowledge/datastore/LocalAuthDataSource.kt new file mode 100644 index 00000000..44d3cef6 --- /dev/null +++ b/core/datastore/src/main/java/com/stackknowledge/datastore/LocalAuthDataSource.kt @@ -0,0 +1,38 @@ +package com.stackknowledge.datastore + +import kotlinx.coroutines.flow.Flow + +interface LocalAuthDataSource { + // AccessToken + suspend fun getAccessToken(): Flow + + suspend fun setAccessToken(accessToken: String) + + suspend fun removeAccessToken() + + // AccessTime + suspend fun getAccessTime(): Flow + + suspend fun setAccessTime(accessTime: String) + + suspend fun removeAccessTime() + + // RefreshToken + suspend fun getRefreshToken(): Flow + + suspend fun setRefreshToken(refreshToken: String) + + suspend fun removeRefreshToken() + + // RefreshTime + suspend fun getRefreshTime(): Flow + + suspend fun setRefreshTime(refreshTime: String) + + suspend fun removeRefreshTime() + + // Authority + suspend fun setAuthorityInfo(authority: String) + + suspend fun getAuthorityInfo(): Flow +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/stackknowledge/datastore/LocalAuthDataSourceImpl.kt b/core/datastore/src/main/java/com/stackknowledge/datastore/LocalAuthDataSourceImpl.kt new file mode 100644 index 00000000..c4af7226 --- /dev/null +++ b/core/datastore/src/main/java/com/stackknowledge/datastore/LocalAuthDataSourceImpl.kt @@ -0,0 +1,87 @@ +package com.stackknowledge.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import com.stackknowledge.datastore.key.AuthPreferenceKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class LocalAuthDataSourceImpl @Inject constructor( + private val dataStore: DataStore +): LocalAuthDataSource { + override suspend fun getAccessToken(): Flow = dataStore.data.map { + it[AuthPreferenceKey.ACCESS_TOKEN] ?: "" + } + + override suspend fun setAccessToken(accessToken: String) { + dataStore.edit { + it[AuthPreferenceKey.ACCESS_TOKEN] = accessToken + } + } + + override suspend fun removeAccessToken() { + dataStore.edit { + it.remove(AuthPreferenceKey.ACCESS_TOKEN) + } + } + + override suspend fun getAccessTime(): Flow = dataStore.data.map { + it[AuthPreferenceKey.ACCESS_TIME] ?: "" + } + + override suspend fun setAccessTime(accessTime: String) { + dataStore.edit { + it[AuthPreferenceKey.ACCESS_TIME] = accessTime + } + } + + override suspend fun removeAccessTime() { + dataStore.edit { + it.remove(AuthPreferenceKey.ACCESS_TIME) + } + } + + override suspend fun getRefreshToken(): Flow = dataStore.data.map { + it[AuthPreferenceKey.REFRESH_TOKEN] ?: "" + } + + override suspend fun setRefreshToken(refreshToken: String) { + dataStore.edit { + it[AuthPreferenceKey.REFRESH_TOKEN] = refreshToken + } + } + + override suspend fun removeRefreshToken() { + dataStore.edit { + it.remove(AuthPreferenceKey.REFRESH_TOKEN) + } + } + + override suspend fun getRefreshTime(): Flow = dataStore.data.map { + it[AuthPreferenceKey.REFRESH_TIME] ?: "" + } + + override suspend fun setRefreshTime(refreshTime: String) { + dataStore.edit { + it[AuthPreferenceKey.REFRESH_TIME] = refreshTime + } + } + + override suspend fun removeRefreshTime() { + dataStore.edit { + it.remove(AuthPreferenceKey.REFRESH_TIME) + } + } + + override suspend fun getAuthorityInfo(): Flow = dataStore.data.map { + it[AuthPreferenceKey.AUTHORITY] ?: "" + } + + override suspend fun setAuthorityInfo(authority: String) { + dataStore.edit { + it[AuthPreferenceKey.AUTHORITY] = authority + } + } +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/stackknowledge/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/stackknowledge/datastore/di/DataStoreModule.kt new file mode 100644 index 00000000..983998c7 --- /dev/null +++ b/core/datastore/src/main/java/com/stackknowledge/datastore/di/DataStoreModule.kt @@ -0,0 +1,30 @@ +package com.stackknowledge.datastore.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + @Provides + @Singleton + fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore { + return PreferenceDataStoreFactory.create( + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { emptyPreferences() } + ), + produceFile = { context.preferencesDataStoreFile("autoDataStore") } + ) + } +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/stackknowledge/datastore/di/LocalDataSourceModule.kt b/core/datastore/src/main/java/com/stackknowledge/datastore/di/LocalDataSourceModule.kt new file mode 100644 index 00000000..73a42149 --- /dev/null +++ b/core/datastore/src/main/java/com/stackknowledge/datastore/di/LocalDataSourceModule.kt @@ -0,0 +1,18 @@ +package com.stackknowledge.datastore.di + +import com.stackknowledge.datastore.LocalAuthDataSource +import com.stackknowledge.datastore.LocalAuthDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class LocalDataSourceModule { + @Binds + abstract fun provideLocalAuthDataSource( + localAuthDataSourceImpl: LocalAuthDataSourceImpl + ): LocalAuthDataSource +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/stackknowledge/datastore/key/AuthPreferenceKey.kt b/core/datastore/src/main/java/com/stackknowledge/datastore/key/AuthPreferenceKey.kt new file mode 100644 index 00000000..d6c9d522 --- /dev/null +++ b/core/datastore/src/main/java/com/stackknowledge/datastore/key/AuthPreferenceKey.kt @@ -0,0 +1,16 @@ +package com.stackknowledge.datastore.key + +import androidx.datastore.preferences.core.stringPreferencesKey + +object AuthPreferenceKey { + + val ACCESS_TOKEN = stringPreferencesKey("access_token") + + val ACCESS_TIME = stringPreferencesKey("access_time") + + val REFRESH_TOKEN = stringPreferencesKey("refresh_token") + + val REFRESH_TIME = stringPreferencesKey("refresh_time") + + val AUTHORITY = stringPreferencesKey("authority") +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/stackknowledge/proto/authToken.proto b/core/datastore/src/main/java/com/stackknowledge/proto/authToken.proto deleted file mode 100644 index 8978ca71..00000000 --- a/core/datastore/src/main/java/com/stackknowledge/proto/authToken.proto +++ /dev/null @@ -1,12 +0,0 @@ - syntax = "proto3"; - - option java_package = "com.msg.datastore"; - option java_multiple_files = true; - - message AuthToken { - string accessToken = 1; - string accessExp = 2; - string refreshToken = 3; - string refreshExp = 4; - string authority = 5; - } \ No newline at end of file diff --git a/core/design-system/src/main/java/com/stackknowledge/design_system/component/button/GoogleButton.kt b/core/design-system/src/main/java/com/stackknowledge/design_system/component/button/GoogleButton.kt index a71d3dbc..5c1eb76e 100644 --- a/core/design-system/src/main/java/com/stackknowledge/design_system/component/button/GoogleButton.kt +++ b/core/design-system/src/main/java/com/stackknowledge/design_system/component/button/GoogleButton.kt @@ -24,11 +24,12 @@ import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme @Composable fun GoogleButton( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onClick: () -> Unit = {} ) { StackKnowledgeAndroidTheme { colors, typography -> Button( - onClick = {}, + onClick = onClick, modifier = modifier .fillMaxWidth() .clip(shape = RoundedCornerShape(10.dp)) diff --git a/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LoginStudentUseCase.kt b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LoginStudentUseCase.kt new file mode 100644 index 00000000..cf896537 --- /dev/null +++ b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LoginStudentUseCase.kt @@ -0,0 +1,14 @@ +package com.stackknowledge.usecase.auth + +import com.stackknowledge.repository.auth.AuthRepository +import kotlinx.coroutines.flow.Flow +import remote.request.auth.LoginRequestModel +import remote.response.auth.LoginResponseModel +import javax.inject.Inject + +class LoginStudentUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + operator fun invoke(body: LoginRequestModel): Flow = + authRepository.loginStudent(body = body) +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LoginTeacherUseCase.kt b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LoginTeacherUseCase.kt new file mode 100644 index 00000000..fabdf4f4 --- /dev/null +++ b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LoginTeacherUseCase.kt @@ -0,0 +1,15 @@ +package com.stackknowledge.usecase.auth + +import com.stackknowledge.repository.auth.AuthRepository +import kotlinx.coroutines.flow.Flow +import remote.request.auth.LoginRequestModel +import remote.response.auth.LoginResponseModel +import javax.inject.Inject + +class LoginTeacherUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + operator fun invoke(body: LoginRequestModel): Flow = + authRepository.loginTeacher(body = body) + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LogoutUseCase.kt b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LogoutUseCase.kt new file mode 100644 index 00000000..a098b988 --- /dev/null +++ b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/LogoutUseCase.kt @@ -0,0 +1,12 @@ +package com.stackknowledge.usecase.auth + +import com.stackknowledge.repository.auth.AuthRepository +import javax.inject.Inject + +class LogoutUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + operator fun invoke() = kotlin.runCatching { + authRepository.logout() + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/SaveTokenUseCase.kt b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/SaveTokenUseCase.kt new file mode 100644 index 00000000..5d809ee7 --- /dev/null +++ b/core/domain/src/main/kotlin/com/stackknowledge/usecase/auth/SaveTokenUseCase.kt @@ -0,0 +1,13 @@ +package com.stackknowledge.usecase.auth + +import com.stackknowledge.repository.auth.AuthRepository +import remote.response.auth.LoginResponseModel +import javax.inject.Inject + +class SaveTokenUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(token: LoginResponseModel) = kotlin.runCatching { + authRepository.saveToken(token = token) + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/stackknowledge/usecase/exception/HttpException.kt b/core/domain/src/main/kotlin/com/stackknowledge/usecase/exception/HttpException.kt new file mode 100644 index 00000000..b9fd36b5 --- /dev/null +++ b/core/domain/src/main/kotlin/com/stackknowledge/usecase/exception/HttpException.kt @@ -0,0 +1,42 @@ +package com.stackknowledge.usecase.exception + +class BadRequestException( + override val message: String? +) : RuntimeException() + +class UnauthorizedException( + override val message: String? +) : RuntimeException() + +class ForBiddenException( + override val message: String? +) : RuntimeException() + +class NotFoundException( + override val message: String? +) : RuntimeException() + +class NotAcceptableException( + override val message: String? +) : RuntimeException() + +class ConflictException( + override val message: String? +) : RuntimeException() + +class TimeOutException( + override val message: String? +) : RuntimeException() + +class ServerException( + override val message: String? +) : RuntimeException() + +class OtherException( + override val message: String?, + val code: Int +) : RuntimeException() + +class UnknownException( + override val message: String? +) : RuntimeException() \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/stackknowledge/usecase/exception/NeedLoginException.kt b/core/domain/src/main/kotlin/com/stackknowledge/usecase/exception/NeedLoginException.kt new file mode 100644 index 00000000..fbeb5280 --- /dev/null +++ b/core/domain/src/main/kotlin/com/stackknowledge/usecase/exception/NeedLoginException.kt @@ -0,0 +1,8 @@ +package com.stackknowledge.usecase.exception + +import java.io.IOException + +class NeedLoginException : IOException() { + override val message: String + get() = "토큰이 만료되었습니다. 다시 로그인 해 주세요" +} \ No newline at end of file diff --git a/core/model/src/main/kotlin/enumdatatype/Authority.kt b/core/model/src/main/kotlin/enumdata/Authority.kt similarity index 53% rename from core/model/src/main/kotlin/enumdatatype/Authority.kt rename to core/model/src/main/kotlin/enumdata/Authority.kt index 4049a8d7..64c32a62 100644 --- a/core/model/src/main/kotlin/enumdatatype/Authority.kt +++ b/core/model/src/main/kotlin/enumdata/Authority.kt @@ -1,6 +1,6 @@ -package enumdatatype +package enumdata enum class Authority { + ROLE_TEACHER, ROLE_STUDENT, - ROLE_TEACHER } \ No newline at end of file diff --git a/core/model/src/main/kotlin/enumdatatype/MissionStatus.kt b/core/model/src/main/kotlin/enumdata/MissionStatus.kt similarity index 77% rename from core/model/src/main/kotlin/enumdatatype/MissionStatus.kt rename to core/model/src/main/kotlin/enumdata/MissionStatus.kt index 842feaed..1a45c91f 100644 --- a/core/model/src/main/kotlin/enumdatatype/MissionStatus.kt +++ b/core/model/src/main/kotlin/enumdata/MissionStatus.kt @@ -1,4 +1,4 @@ -package enumdatatype +package enumdata enum class MissionStatus { CLOSED, diff --git a/core/model/src/main/kotlin/remote/request/auth/LoginRequestModel.kt b/core/model/src/main/kotlin/remote/request/auth/LoginRequestModel.kt new file mode 100644 index 00000000..e002a47c --- /dev/null +++ b/core/model/src/main/kotlin/remote/request/auth/LoginRequestModel.kt @@ -0,0 +1,5 @@ +package remote.request.auth + +data class LoginRequestModel( + val code: String, +) diff --git a/core/model/src/main/kotlin/remote/response/auth/LoginResponseModel.kt b/core/model/src/main/kotlin/remote/response/auth/LoginResponseModel.kt new file mode 100644 index 00000000..2517da7e --- /dev/null +++ b/core/model/src/main/kotlin/remote/response/auth/LoginResponseModel.kt @@ -0,0 +1,10 @@ +package remote.response.auth + +import enumdata.Authority + +data class LoginResponseModel( + val accessToken: String, + val refreshToken: String, + val expiredAt: String, + val authority: Authority, +) \ No newline at end of file diff --git a/core/model/src/main/kotlin/remote/response/mission/MissionResponseModel.kt b/core/model/src/main/kotlin/remote/response/mission/MissionResponseModel.kt index 01241ca0..a5441003 100644 --- a/core/model/src/main/kotlin/remote/response/mission/MissionResponseModel.kt +++ b/core/model/src/main/kotlin/remote/response/mission/MissionResponseModel.kt @@ -1,8 +1,6 @@ package remote.response.mission -import enumdatatype.MissionStatus import remote.user.UserModel -import java.util.UUID data class MissionResponseModel( val id: String, diff --git a/core/model/src/main/kotlin/remote/response/mission/MissionsModel.kt b/core/model/src/main/kotlin/remote/response/mission/MissionsModel.kt index e650be7f..9800d859 100644 --- a/core/model/src/main/kotlin/remote/response/mission/MissionsModel.kt +++ b/core/model/src/main/kotlin/remote/response/mission/MissionsModel.kt @@ -1,6 +1,6 @@ package remote.response.mission -import enumdatatype.MissionStatus +import enumdata.MissionStatus import remote.user.UserModel import java.util.UUID diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 32ef8df8..a3754ee5 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -1,4 +1,5 @@ import java.io.FileInputStream +import java.io.FileNotFoundException import java.util.Properties @Suppress("DSL_SCOPE_VIOLATION") @@ -14,9 +15,11 @@ android { defaultConfig { buildConfigField("String", "BASE_URL", getApiKey("BASE_URL")) + buildConfigField("String", "GOOGLE_CLIENT_ID", getApiKey("GOOGLE_CLIENT_ID")) + buildConfigField("String", "SCOPE", getApiKey("SCOPE")) } - namespace = "com.msg.network" + namespace = "com.stackknowledge.network" } dependencies { @@ -33,12 +36,13 @@ dependencies { implementation(libs.retrofit.kotlin.serialization) implementation(libs.retrofit.moshi.converter) implementation(libs.moshi) - ksp(libs.retrofit.moshi.codegen) + implementation(libs.retrofit.moshi.codegen) + implementation(libs.moshi.kotlin) } fun getApiKey(propertyKey: String): String { val propFile = rootProject.file("./local.properties") val properties = Properties() properties.load(FileInputStream(propFile)) - return properties.getProperty(propertyKey) + return properties.getProperty(propertyKey) ?: throw IllegalArgumentException("Property $propertyKey not found in local.properties file") } \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/api/AuthAPI.kt b/core/network/src/main/kotlin/com/stackknowledge/api/AuthAPI.kt new file mode 100644 index 00000000..9e6b3179 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/api/AuthAPI.kt @@ -0,0 +1,22 @@ +package com.stackknowledge.api + +import com.stackknowledge.dto.request.auth.LoginRequest +import com.stackknowledge.dto.response.auth.LoginResponse +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.POST + +interface AuthAPI { + @POST("/auth/student") + suspend fun loginStudent( + @Body body: LoginRequest, + ): LoginResponse + + @POST("/auth/teacher") + suspend fun loginTeacher( + @Body body: LoginRequest, + ): LoginResponse + + @DELETE("/auth") + suspend fun logout() +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/datasource/auth/AuthDataSource.kt b/core/network/src/main/kotlin/com/stackknowledge/datasource/auth/AuthDataSource.kt new file mode 100644 index 00000000..efb39c65 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/datasource/auth/AuthDataSource.kt @@ -0,0 +1,11 @@ +package com.stackknowledge.datasource.auth + +import com.stackknowledge.dto.request.auth.LoginRequest +import com.stackknowledge.dto.response.auth.LoginResponse +import kotlinx.coroutines.flow.Flow + +interface AuthDataSource { + fun loginStudent(body: LoginRequest): Flow + fun loginTeacher(body: LoginRequest): Flow + fun logout(): Flow +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/datasource/auth/AuthDataSourceImpl.kt b/core/network/src/main/kotlin/com/stackknowledge/datasource/auth/AuthDataSourceImpl.kt new file mode 100644 index 00000000..ba24fa16 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/datasource/auth/AuthDataSourceImpl.kt @@ -0,0 +1,40 @@ +package com.stackknowledge.datasource.auth + +import android.util.Log +import com.stackknowledge.api.AuthAPI +import com.stackknowledge.dto.request.auth.LoginRequest +import com.stackknowledge.dto.response.auth.LoginResponse +import com.stackknowledge.util.StackKnowledgeApiHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject + +class AuthDataSourceImpl @Inject constructor( + private val authAPI: AuthAPI +) : AuthDataSource { + override fun loginStudent(body: LoginRequest): Flow = flow { + emit( + StackKnowledgeApiHandler() + .httpRequest { authAPI.loginStudent(body = body) } + .sendRequest() + ) + }.flowOn(Dispatchers.IO) + + override fun loginTeacher(body: LoginRequest): Flow = flow { + emit( + StackKnowledgeApiHandler() + .httpRequest { authAPI.loginTeacher(body = body) } + .sendRequest() + ) + }.flowOn(Dispatchers.IO) + + override fun logout(): Flow = flow { + emit( + StackKnowledgeApiHandler() + .httpRequest { authAPI.logout() } + .sendRequest() + ) + }.flowOn(Dispatchers.IO) +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/di/AppConfigModule.kt b/core/network/src/main/kotlin/com/stackknowledge/di/AppConfigModule.kt new file mode 100644 index 00000000..39f1adc3 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/di/AppConfigModule.kt @@ -0,0 +1,20 @@ +package com.stackknowledge.di + +import com.stackknowledge.network.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Named + +@Module +@InstallIn(SingletonComponent::class) +object AppConfigModule { + @Provides + @Named("GOOGLE_CLIENT_ID") + fun provideGoogleClientId(): String = BuildConfig.GOOGLE_CLIENT_ID + + @Provides + @Named("SCOPE") + fun provideScope(): String = BuildConfig.SCOPE +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/di/NetworkModule.kt b/core/network/src/main/kotlin/com/stackknowledge/di/NetworkModule.kt index a1469c19..1ca37f29 100644 --- a/core/network/src/main/kotlin/com/stackknowledge/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/stackknowledge/di/NetworkModule.kt @@ -1,19 +1,21 @@ package com.stackknowledge.di -import com.msg.network.BuildConfig +import android.util.Log import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.stackknowledge.api.AuthAPI import com.stackknowledge.api.ItemAPI import com.stackknowledge.api.MissionAPI import com.stackknowledge.api.OrderAPI import com.stackknowledge.api.SolveAPI import com.stackknowledge.api.StudentAPI import com.stackknowledge.api.UserAPI +import com.stackknowledge.network.BuildConfig import com.stackknowledge.util.AuthInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import okhttp3.CookieJar import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -24,6 +26,10 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object NetworkModule { + @Provides + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor { message -> Log.v("HTTP", message) } + .setLevel(HttpLoggingInterceptor.Level.BODY) @Provides @Singleton @@ -32,7 +38,6 @@ object NetworkModule { authInterceptor: AuthInterceptor, ): OkHttpClient { return OkHttpClient.Builder() - .cookieJar(CookieJar.NO_COOKIES) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) @@ -43,21 +48,15 @@ object NetworkModule { @Provides @Singleton - fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { - return HttpLoggingInterceptor().apply { - level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE - } - } - - @Provides - @Singleton - fun provideMoshiInstance(): Moshi { - return Moshi.Builder().build() + fun provideMoshi(): Moshi { + return Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() } @Provides @Singleton - fun provideConverterFactory(moshi: Moshi): MoshiConverterFactory { + fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory { return MoshiConverterFactory.create(moshi) } @@ -73,6 +72,12 @@ object NetworkModule { .addConverterFactory(moshiConverterFactory) .build() } + + @Provides + @Singleton + fun provideAuthAPI(retrofit: Retrofit): AuthAPI = + retrofit.create(AuthAPI::class.java) + @Provides @Singleton fun provideMissionAPI(retrofit: Retrofit): MissionAPI = diff --git a/core/network/src/main/kotlin/com/stackknowledge/di/RemoteDataSourceModule.kt b/core/network/src/main/kotlin/com/stackknowledge/di/RemoteDataSourceModule.kt index 536698fa..7b553e36 100644 --- a/core/network/src/main/kotlin/com/stackknowledge/di/RemoteDataSourceModule.kt +++ b/core/network/src/main/kotlin/com/stackknowledge/di/RemoteDataSourceModule.kt @@ -1,5 +1,7 @@ package com.stackknowledge.di +import com.stackknowledge.datasource.auth.AuthDataSource +import com.stackknowledge.datasource.auth.AuthDataSourceImpl import com.stackknowledge.datasource.item.ItemDataSource import com.stackknowledge.datasource.item.ItemDataSourceImpl import com.stackknowledge.datasource.mission.MissionDataSource @@ -21,6 +23,12 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class RemoteDataSourceModule { + @Binds + @Singleton + abstract fun bindAuthDataSource( + authDataSourceImpl: AuthDataSourceImpl + ): AuthDataSource + @Binds @Singleton abstract fun bindMissionDataSource( diff --git a/core/network/src/main/kotlin/com/stackknowledge/dto/request/auth/LoginRequest.kt b/core/network/src/main/kotlin/com/stackknowledge/dto/request/auth/LoginRequest.kt new file mode 100644 index 00000000..f6a75c2f --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/dto/request/auth/LoginRequest.kt @@ -0,0 +1,9 @@ +package com.stackknowledge.dto.request.auth + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LoginRequest( + @Json(name = "code") val code: String, +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/dto/response/auth/LoginResponse.kt b/core/network/src/main/kotlin/com/stackknowledge/dto/response/auth/LoginResponse.kt new file mode 100644 index 00000000..c3a91ab6 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/dto/response/auth/LoginResponse.kt @@ -0,0 +1,13 @@ +package com.stackknowledge.dto.response.auth + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import enumdata.Authority + +@JsonClass(generateAdapter = true) +data class LoginResponse( + @Json(name = "accessToken") val accessToken: String, + @Json(name = "refreshToken") val refreshToken: String, + @Json(name = "expiredAt") val expiredAt: String, + @Json(name = "authority") val authority: Authority, +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/GetMissionListResponse.kt b/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/GetMissionListResponse.kt new file mode 100644 index 00000000..73b90378 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/GetMissionListResponse.kt @@ -0,0 +1,18 @@ +package com.stackknowledge.dto.response.mission + +import enumdata.MissionStatus +import java.util.UUID + +data class GetMissionListResponse( + val id: UUID, + val title: String, + val point: Int, + val missionStatus: MissionStatus, + val user: User, +) { + data class User( + val id: UUID, + val name: String, + val profileImage: String?, + ) +} diff --git a/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/MissionResponse.kt b/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/MissionResponse.kt index dbdacef4..623ecbb4 100644 --- a/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/MissionResponse.kt +++ b/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/MissionResponse.kt @@ -3,9 +3,6 @@ package com.stackknowledge.dto.response.mission import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.stackknowledge.dto.user.User -import enumdatatype.MissionStatus -import remote.user.UserModel -import java.util.UUID @JsonClass(generateAdapter = true) data class MissionResponse( diff --git a/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/Missions.kt b/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/Missions.kt new file mode 100644 index 00000000..8f387747 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/dto/response/mission/Missions.kt @@ -0,0 +1,16 @@ +package com.stackknowledge.dto.response.mission + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import enumdata.MissionStatus +import remote.user.UserModel +import java.util.UUID + +@JsonClass(generateAdapter = true) +data class Missions( + @Json(name = "id") val id: UUID, + @Json(name = "title") val title: String, + @Json(name = "point") val point: Int, + @Json(name = "missionStatus") val missionStatus: MissionStatus, + @Json(name = "user") val user: UserModel, +) diff --git a/core/network/src/main/kotlin/com/stackknowledge/mapper/request/auth/LoginRequestMapper.kt b/core/network/src/main/kotlin/com/stackknowledge/mapper/request/auth/LoginRequestMapper.kt new file mode 100644 index 00000000..bfda8797 --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/mapper/request/auth/LoginRequestMapper.kt @@ -0,0 +1,8 @@ +package com.stackknowledge.mapper.request.auth + +import com.stackknowledge.dto.request.auth.LoginRequest +import remote.request.auth.LoginRequestModel + +fun LoginRequestModel.toDto(): LoginRequest = LoginRequest( + code = this.code, +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/mapper/response/auth/LoginResponseMapper.kt b/core/network/src/main/kotlin/com/stackknowledge/mapper/response/auth/LoginResponseMapper.kt new file mode 100644 index 00000000..75458b0f --- /dev/null +++ b/core/network/src/main/kotlin/com/stackknowledge/mapper/response/auth/LoginResponseMapper.kt @@ -0,0 +1,11 @@ +package com.stackknowledge.mapper.response.auth + +import com.stackknowledge.dto.response.auth.LoginResponse +import remote.response.auth.LoginResponseModel + +fun LoginResponse.toModel(): LoginResponseModel = LoginResponseModel( + accessToken = this.accessToken, + refreshToken = this.refreshToken, + expiredAt = this.expiredAt, + authority = this.authority, +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/stackknowledge/util/AuthInterceptor.kt b/core/network/src/main/kotlin/com/stackknowledge/util/AuthInterceptor.kt index 99d2616f..0911f817 100644 --- a/core/network/src/main/kotlin/com/stackknowledge/util/AuthInterceptor.kt +++ b/core/network/src/main/kotlin/com/stackknowledge/util/AuthInterceptor.kt @@ -1,19 +1,90 @@ package com.stackknowledge.util +import com.example.common.exception.NeedLoginException +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.stackknowledge.network.BuildConfig +import com.stackknowledge.datastore.LocalAuthDataSource +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody import okhttp3.Response -import util.ResourceKeys import javax.inject.Inject -class AuthInterceptor @Inject constructor(): Interceptor { +class AuthInterceptor @Inject constructor( + private val dataSource: LocalAuthDataSource +): Interceptor { + + private val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + data class TokenResponse( + val accessToken: String, + val refreshToken: String, + val expiredAt: String + ) + override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val builder = request.newBuilder() + val currentTime = System.currentTimeMillis().toLocalDateTime() + val ignorePath = listOf("/auth") + val ignoreMethod = listOf("POST") + val path = request.url.encodedPath + val method = request.method + + ignorePath.forEachIndexed { index, s -> + if (path.contains(s) && ignoreMethod[index] == method) + return chain.proceed(request) + } runBlocking { - builder.addHeader("Authorization", "${ResourceKeys.BEARER} ") + val refreshTime = dataSource.getRefreshTime().first().replace("\"", "") + val accessTime = dataSource.getAccessTime().first().replace("\"", "") + + if (refreshTime == "") { + return@runBlocking + } + + if (currentTime != null) { + if (currentTime.isAfter(refreshTime.toLocalDateTime())) { + throw NeedLoginException() + } + } + + // access 토큰 재 발급 + if (currentTime != null) { + if (currentTime.isAfter(accessTime.toLocalDateTime())) { + val client = OkHttpClient() + val refreshRequest = Request.Builder() + .url(BuildConfig.BASE_URL + "/auth") + .patch(chain.request().body ?: RequestBody.create(null, byteArrayOf())) + .addHeader( + "Refresh-Token", + dataSource.getRefreshToken().first().replace("\"", "") + ) + .build() + val response = client.newCall(refreshRequest).execute() + if (response.isSuccessful) { + val adapter = moshi.adapter(TokenResponse::class.java) + val tokenResponse = adapter.fromJson(response.body!!.string()) + + tokenResponse?.let { + dataSource.setAccessToken(it.accessToken) + dataSource.setRefreshToken(it.refreshToken) + dataSource.setAccessTime(it.expiredAt) + dataSource.setRefreshTime(it.expiredAt) + } ?: throw NeedLoginException() + } else throw NeedLoginException() + } + } + val accessToken = dataSource.getAccessToken().first().replace("\"", "") + builder.addHeader("Authorization", "Bearer $accessToken") } return chain.proceed(builder.build()) } -} \ No newline at end of file +} diff --git a/core/ui/src/main/java/com/minstone/ui/navigation/StackKnowledgeBottomNavigation.kt b/core/ui/src/main/java/com/minstone/ui/navigation/StackKnowledgeBottomNavigation.kt index cfb414dc..96d5b25e 100644 --- a/core/ui/src/main/java/com/minstone/ui/navigation/StackKnowledgeBottomNavigation.kt +++ b/core/ui/src/main/java/com/minstone/ui/navigation/StackKnowledgeBottomNavigation.kt @@ -1,7 +1,5 @@ package com.minstone.ui.navigation -import android.icu.text.TimeZoneNames.NameType -import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,7 +19,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.stackknowledge.design_system.R import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme -import enumdatatype.Authority +import enumdata.Authority enum class NavigateType(val value: String) { HOME("home"), diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index 65db4e8a..537d78c1 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -6,4 +6,9 @@ plugins { android { namespace = "com.stackknowledge.login" +} + +dependencies { + implementation(libs.google.services) + implementation(libs.play.services.auth) } \ No newline at end of file diff --git a/feature/login/src/androidTest/java/com/stackknowledge/login/ExampleInstrumentedTest.kt b/feature/login/src/androidTest/java/com/stackknowledge/login/ExampleInstrumentedTest.kt deleted file mode 100644 index 6ee0b701..00000000 --- a/feature/login/src/androidTest/java/com/stackknowledge/login/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.stackknowledge.login - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.teamgrapefruit.login.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/feature/login/src/main/java/com/stackknowledge/login/LoginScreen.kt b/feature/login/src/main/java/com/stackknowledge/login/LoginScreen.kt index 98150860..6e18116d 100644 --- a/feature/login/src/main/java/com/stackknowledge/login/LoginScreen.kt +++ b/feature/login/src/main/java/com/stackknowledge/login/LoginScreen.kt @@ -1,5 +1,7 @@ package com.stackknowledge.login +import android.util.Log +import androidx.activity.ComponentActivity import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,38 +14,61 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.Surface 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.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 androidx.lifecycle.compose.collectAsStateWithLifecycle import com.stackknowledge.design_system.R import com.stackknowledge.design_system.component.button.GoogleButton import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.login.background.LoginBackground +import com.stackknowledge.login.viewmodel.AuthViewModel +import com.stackknowledge.login.viewmodel.uistate.LoginUiState @Composable -internal fun LoginScreenRoute() { - LoginScreen() +internal fun LoginRoute( + onSuccess: () -> Unit, + onGoogleLoginButtonClicked: () -> Unit, + viewModel: AuthViewModel = hiltViewModel(LocalContext.current as ComponentActivity), +) { + val loginUiState by viewModel.loginUiState.collectAsStateWithLifecycle() + + LoginScreen( + onGoogleLoginButtonClicked = onGoogleLoginButtonClicked, + loginUiState = loginUiState, + viewModel = viewModel, + onLoginSuccess = onSuccess + ) } @Composable private fun LoginScreen( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: AuthViewModel = hiltViewModel(), + onGoogleLoginButtonClicked: () -> Unit = {}, + loginUiState: LoginUiState, + onLoginSuccess: () -> Unit = {}, ) { StackKnowledgeAndroidTheme { colors, typography -> Surface { Column( modifier = modifier.fillMaxSize() ) { - Box() { + Box { LoginBackground() + Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = modifier.height(157.dp)) + Image( painter = painterResource(R.drawable.stack_knowledge_logo), contentDescription = "Stack Knowledge Logo", @@ -51,31 +76,59 @@ private fun LoginScreen( .width(50.dp) .height(50.dp) ) + Spacer(modifier = modifier.height(20.dp)) + Text( text = stringResource(R.string.app_title), style = typography.titleLarge, color = colors.BLACK ) + Spacer(modifier = modifier.height(360.dp)) + Column( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp) ) { GoogleButton( - modifier = modifier.height(60.dp) + modifier = modifier.height(60.dp), + onClick = { + onGoogleLoginButtonClicked() + } ) } } } + + when(loginUiState) { + is LoginUiState.Success -> { + val tokenResponse = loginUiState.loginResponseModel + + viewModel.saveToken(tokenResponse) + onLoginSuccess() + } + is LoginUiState.Error -> { + // Login 실패 처리 (임의) + Log.e("Login Error", "Login Error") + } + LoginUiState.Loading -> { + // Loading 처리 (임의) + Log.e("Login Loading", "Login Loading") + } + } } } } } + @Preview @Composable fun LoginScreenPre() { - LoginScreen() + LoginScreen( + onGoogleLoginButtonClicked = {}, + loginUiState = LoginUiState.Loading + ) } \ No newline at end of file diff --git a/feature/login/src/main/java/com/stackknowledge/login/RoleCheckScreen.kt b/feature/login/src/main/java/com/stackknowledge/login/RoleCheckScreen.kt index 37da72b2..cab874a0 100644 --- a/feature/login/src/main/java/com/stackknowledge/login/RoleCheckScreen.kt +++ b/feature/login/src/main/java/com/stackknowledge/login/RoleCheckScreen.kt @@ -1,5 +1,7 @@ package com.stackknowledge.login + +import androidx.activity.ComponentActivity import androidx.compose.foundation.background import com.stackknowledge.design_system.R import androidx.compose.foundation.layout.Box @@ -15,28 +17,41 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.stackknowledge.design_system.component.button.StackKnowledgeButton import com.stackknowledge.design_system.component.button.enumclass.ButtonState import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.login.background.LoginBackground +import com.stackknowledge.login.viewmodel.AuthViewModel @Composable -internal fun RoleCheckScreenRoute( - onRoleClick: () -> Unit, +internal fun RoleCheckRoute( + onRoleButtonClick: () -> Unit, +authViewModel: AuthViewModel = hiltViewModel(LocalContext.current as ComponentActivity) ) { - RoleCheckScreen( - onRoleClick = onRoleClick - ) + with(authViewModel) { + RoleCheckScreen( + onTeacherButtonClick = { teacherRoleBoolean -> + onRoleButtonClick() + isTeacher.value = teacherRoleBoolean + }, + onStudentButtonClick = { studentRoleBoolean -> + onRoleButtonClick() + isStudent.value = studentRoleBoolean + } + ) + } } - @Composable private fun RoleCheckScreen( modifier: Modifier = Modifier, - onRoleClick: () -> Unit, + onTeacherButtonClick: (Boolean) -> Unit, + onStudentButtonClick: (Boolean) -> Unit, ) { StackKnowledgeAndroidTheme { colors, typography -> Box( @@ -69,7 +84,9 @@ private fun RoleCheckScreen( modifier = modifier .height(60.dp) .weight(1f), - onClick = onRoleClick + onClick = { + onStudentButtonClick(true) + } ) Spacer(modifier = modifier.width(8.dp)) @@ -80,7 +97,9 @@ private fun RoleCheckScreen( modifier = modifier .height(60.dp) .weight(1f), - onClick = onRoleClick + onClick = { + onTeacherButtonClick(true) + } ) } @@ -94,6 +113,7 @@ private fun RoleCheckScreen( @Composable fun RoleCheckScreenPre() { RoleCheckScreen( - onRoleClick = {} + onTeacherButtonClick = {}, + onStudentButtonClick = {}, ) } \ No newline at end of file diff --git a/feature/login/src/main/java/com/stackknowledge/login/navigation/LoginNavigation.kt b/feature/login/src/main/java/com/stackknowledge/login/navigation/LoginNavigation.kt index 9d03e5e8..6e1ece0c 100644 --- a/feature/login/src/main/java/com/stackknowledge/login/navigation/LoginNavigation.kt +++ b/feature/login/src/main/java/com/stackknowledge/login/navigation/LoginNavigation.kt @@ -4,8 +4,8 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import com.stackknowledge.login.LoginScreenRoute -import com.stackknowledge.login.RoleCheckScreenRoute +import com.stackknowledge.login.LoginRoute +import com.stackknowledge.login.RoleCheckRoute const val loginRoute = "login_route" const val roleCheckRoute = "role_check_route" @@ -14,9 +14,15 @@ fun NavController.navigateToLogin(navOptions: NavOptions? = null) { this.navigate(loginRoute, navOptions) } -fun NavGraphBuilder.loginScreen() { +fun NavGraphBuilder.loginScreen( + onSuccess: () -> Unit = {}, + onLoginButtonClick: () -> Unit = {}, +) { composable(route = loginRoute) { - LoginScreenRoute() + LoginRoute( + onSuccess = onSuccess, + onGoogleLoginButtonClicked = onLoginButtonClick + ) } } @@ -25,11 +31,11 @@ fun NavController.navigateToRoleCheck(navOptions: NavOptions? = null) { } fun NavGraphBuilder.roleCheckScreen( - onRoleClick: () -> Unit, + onRoleButtonClick: () -> Unit, ) { composable(route = roleCheckRoute) { - RoleCheckScreenRoute( - onRoleClick = onRoleClick + RoleCheckRoute( + onRoleButtonClick = onRoleButtonClick ) } } \ No newline at end of file diff --git a/feature/login/src/main/java/com/stackknowledge/login/viewmodel/AuthViewModel.kt b/feature/login/src/main/java/com/stackknowledge/login/viewmodel/AuthViewModel.kt new file mode 100644 index 00000000..dc86fa6e --- /dev/null +++ b/feature/login/src/main/java/com/stackknowledge/login/viewmodel/AuthViewModel.kt @@ -0,0 +1,75 @@ +package com.stackknowledge.login.viewmodel + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.common.result.Result +import com.example.common.result.asResult +import com.example.common.util.Event +import com.example.common.util.errorHandling +import com.stackknowledge.login.viewmodel.uistate.LoginUiState +import com.stackknowledge.usecase.auth.SaveTokenUseCase +import com.stackknowledge.usecase.auth.LoginStudentUseCase +import com.stackknowledge.usecase.auth.LoginTeacherUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import remote.request.auth.LoginRequestModel +import remote.response.auth.LoginResponseModel +import javax.inject.Inject + +@HiltViewModel +class AuthViewModel @Inject constructor( + private val loginStudentUseCase: LoginStudentUseCase, + private val loginTeacherUseCase: LoginTeacherUseCase, + private val saveTokenUseCase: SaveTokenUseCase, +) : ViewModel() { + private val _saveTokenRequest = MutableStateFlow>(Event.Loading) + internal val saveTokenRequest = _saveTokenRequest.asStateFlow() + + private val _loginUiState = MutableStateFlow(LoginUiState.Loading) + internal val loginUiState = _loginUiState.asStateFlow() + + var isTeacher = mutableStateOf(false) + private set + + var isStudent = mutableStateOf(false) + private set + + fun loginStudent(body: LoginRequestModel) = viewModelScope.launch { + loginStudentUseCase(body = body) + .asResult() + .collectLatest { result -> + when (result) { + is Result.Loading -> _loginUiState.value = LoginUiState.Loading + is Result.Success -> _loginUiState.value = LoginUiState.Success(result.data) + is Result.Error -> _loginUiState.value = LoginUiState.Error(result.exception) + } + } + } + + fun loginTeacher(body: LoginRequestModel) = viewModelScope.launch { + loginTeacherUseCase(body = body) + .asResult() + .collectLatest { result -> + when (result) { + is Result.Loading -> _loginUiState.value = LoginUiState.Loading + is Result.Success -> _loginUiState.value = LoginUiState.Success(result.data) + is Result.Error -> _loginUiState.value = LoginUiState.Error(result.exception) + } + + } + } + + internal fun saveToken(token: LoginResponseModel) = viewModelScope.launch { + saveTokenUseCase( + token = token + ).onSuccess { + _saveTokenRequest.value = Event.Success() + }.onFailure { + _saveTokenRequest.value = it.errorHandling() + } + } +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/stackknowledge/login/viewmodel/uistate/LoginUiState.kt b/feature/login/src/main/java/com/stackknowledge/login/viewmodel/uistate/LoginUiState.kt new file mode 100644 index 00000000..89f1a33f --- /dev/null +++ b/feature/login/src/main/java/com/stackknowledge/login/viewmodel/uistate/LoginUiState.kt @@ -0,0 +1,9 @@ +package com.stackknowledge.login.viewmodel.uistate + +import remote.response.auth.LoginResponseModel + +sealed interface LoginUiState { + object Loading : LoginUiState + data class Success(val loginResponseModel: LoginResponseModel) : LoginUiState + data class Error(val exception: Throwable) : LoginUiState +} \ No newline at end of file diff --git a/feature/login/src/test/java/com/stackknowledge/login/ExampleUnitTest.kt b/feature/login/src/test/java/com/stackknowledge/login/ExampleUnitTest.kt deleted file mode 100644 index 54deaa69..00000000 --- a/feature/login/src/test/java/com/stackknowledge/login/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.stackknowledge.login - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/feature/main/src/main/java/com/stackknowledge/main/MainPageScreen.kt b/feature/main/src/main/java/com/stackknowledge/main/MainPageScreen.kt index e713bb2e..19b0b9eb 100644 --- a/feature/main/src/main/java/com/stackknowledge/main/MainPageScreen.kt +++ b/feature/main/src/main/java/com/stackknowledge/main/MainPageScreen.kt @@ -34,7 +34,7 @@ import com.stackknowledge.main.component.StackKnowledgePager import com.stackknowledge.main.viewModel.MainViewModel import com.stackknowledge.main.viewModel.uistate.GetMissionUiState import com.stackknowledge.main.viewModel.uistate.GetRankingUiState -import enumdatatype.Authority +import enumdata.Authority @Composable internal fun MainPageRoute( diff --git a/feature/main/src/main/java/com/stackknowledge/main/component/RankingList.kt b/feature/main/src/main/java/com/stackknowledge/main/component/RankingList.kt index 80dfee42..50e75f6e 100644 --- a/feature/main/src/main/java/com/stackknowledge/main/component/RankingList.kt +++ b/feature/main/src/main/java/com/stackknowledge/main/component/RankingList.kt @@ -15,12 +15,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme -import com.stackknowledge.main.viewModel.uistate.GetMissionUiState import com.stackknowledge.main.viewModel.uistate.GetRankingUiState -import enumdatatype.Authority @Composable fun RankingList( diff --git a/feature/main/src/main/java/com/stackknowledge/main/navigation/MainNavigation.kt b/feature/main/src/main/java/com/stackknowledge/main/navigation/MainNavigation.kt index 7f71a6ff..53326e28 100644 --- a/feature/main/src/main/java/com/stackknowledge/main/navigation/MainNavigation.kt +++ b/feature/main/src/main/java/com/stackknowledge/main/navigation/MainNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.stackknowledge.main.MainPageRoute -import enumdatatype.Authority +import enumdata.Authority const val mainPageRoute = "main_page_route" diff --git a/feature/mission/src/main/java/com/stackkowledge/mission/CreateMissionScreen.kt b/feature/mission/src/main/java/com/stackkowledge/mission/CreateMissionScreen.kt index 7d100c54..f19c5e5b 100644 --- a/feature/mission/src/main/java/com/stackkowledge/mission/CreateMissionScreen.kt +++ b/feature/mission/src/main/java/com/stackkowledge/mission/CreateMissionScreen.kt @@ -1,7 +1,5 @@ package com.stackkowledge.mission -import android.util.Log -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,10 +30,9 @@ import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackkowledge.mission.component.CreateMissionTimer import com.stackkowledge.mission.component.InputMission import com.stackkowledge.mission.component.InputTitle -import com.stackkowledge.mission.viewmodel.uistate.CreateMissionUiState import com.stackkowledge.mission.util.isValidNumber import com.stackkowledge.mission.viewmodel.MissionViewModel -import enumdatatype.Authority +import enumdata.Authority import remote.request.mission.CreateMissionRequestModel @Composable diff --git a/feature/mission/src/main/java/com/stackkowledge/mission/EntireMissionScreen.kt b/feature/mission/src/main/java/com/stackkowledge/mission/EntireMissionScreen.kt index e8c5f583..63608ec4 100644 --- a/feature/mission/src/main/java/com/stackkowledge/mission/EntireMissionScreen.kt +++ b/feature/mission/src/main/java/com/stackkowledge/mission/EntireMissionScreen.kt @@ -21,8 +21,8 @@ import com.stackknowledge.design_system.component.topbar.StackKnowledgeTopBar import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackkowledge.mission.component.EntireMissionList import com.stackkowledge.mission.viewmodel.MissionViewModel +import enumdata.Authority import com.stackkowledge.mission.viewmodel.uistate.GetMissionUiState -import enumdatatype.Authority @Composable internal fun EntireMissionRoute( diff --git a/feature/mission/src/main/java/com/stackkowledge/mission/ResolveMissionScreen.kt b/feature/mission/src/main/java/com/stackkowledge/mission/ResolveMissionScreen.kt index a4b50da5..18e64128 100644 --- a/feature/mission/src/main/java/com/stackkowledge/mission/ResolveMissionScreen.kt +++ b/feature/mission/src/main/java/com/stackkowledge/mission/ResolveMissionScreen.kt @@ -30,7 +30,6 @@ import com.minstone.ui.navigation.StackKnowledgeBottomNavigation import com.stackknowledge.design_system.component.dialog.StackKnowledgeDialog import com.stackknowledge.design_system.component.topbar.StackKnowledgeTopBar import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme -import enumdatatype.Authority import com.stackknowledge.design_system.R import com.stackknowledge.design_system.component.dialog.EmptyButtonDialog import com.stackknowledge.design_system.component.toast.SuccessToastMessage @@ -40,9 +39,8 @@ import com.stackkowledge.mission.component.MissionTimer import com.stackkowledge.mission.viewmodel.MissionViewModel import com.stackkowledge.mission.viewmodel.SolveMissionViewModel import com.stackkowledge.mission.viewmodel.uistate.DetailMissionUiState -import com.stackkowledge.mission.viewmodel.uistate.SolveMissionUiState +import enumdata.Authority import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import remote.request.solve.SolveRequestModel @Composable diff --git a/feature/mission/src/main/java/com/stackkowledge/mission/navigation/MissionNavigation.kt b/feature/mission/src/main/java/com/stackkowledge/mission/navigation/MissionNavigation.kt index 7ed7681f..39bcd841 100644 --- a/feature/mission/src/main/java/com/stackkowledge/mission/navigation/MissionNavigation.kt +++ b/feature/mission/src/main/java/com/stackkowledge/mission/navigation/MissionNavigation.kt @@ -7,7 +7,7 @@ import androidx.navigation.compose.composable import com.stackkowledge.mission.ResolveMissionRoute import com.stackkowledge.mission.CreateMissionRoute import com.stackkowledge.mission.EntireMissionRoute -import enumdatatype.Authority +import enumdata.Authority const val createMissionRoute = "create_mission_route" const val entireMissionRoute = "entire_mission_route" diff --git a/feature/ranking/src/main/java/com/stackknowledge/ranking/RankingScreen.kt b/feature/ranking/src/main/java/com/stackknowledge/ranking/RankingScreen.kt index b897c806..fc88c306 100644 --- a/feature/ranking/src/main/java/com/stackknowledge/ranking/RankingScreen.kt +++ b/feature/ranking/src/main/java/com/stackknowledge/ranking/RankingScreen.kt @@ -22,10 +22,10 @@ import com.stackknowledge.design_system.component.topbar.StackKnowledgeTopBar import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.ranking.component.RankingList import com.stackknowledge.ranking.component.RankingProfile +import enumdata.Authority import com.stackknowledge.ranking.viewmodel.RankingViewModel import com.stackknowledge.ranking.viewmodel.uistate.GetMyInformationUiState import com.stackknowledge.ranking.viewmodel.uistate.GetRankingUiState -import enumdatatype.Authority @Composable internal fun RankingRoute( diff --git a/feature/ranking/src/main/java/com/stackknowledge/ranking/TeacherRankingScreen.kt b/feature/ranking/src/main/java/com/stackknowledge/ranking/TeacherRankingScreen.kt index 0ac86808..4f4a723e 100644 --- a/feature/ranking/src/main/java/com/stackknowledge/ranking/TeacherRankingScreen.kt +++ b/feature/ranking/src/main/java/com/stackknowledge/ranking/TeacherRankingScreen.kt @@ -21,9 +21,9 @@ import com.minstone.ui.navigation.StackKnowledgeBottomNavigation import com.stackknowledge.design_system.component.topbar.StackKnowledgeTopBar import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.ranking.component.RankingList +import enumdata.Authority import com.stackknowledge.ranking.viewmodel.RankingViewModel import com.stackknowledge.ranking.viewmodel.uistate.GetRankingUiState -import enumdatatype.Authority @Composable internal fun TeacherRankingRoute( diff --git a/feature/ranking/src/main/java/com/stackknowledge/ranking/navigation/RankingNavigation.kt b/feature/ranking/src/main/java/com/stackknowledge/ranking/navigation/RankingNavigation.kt index bd91d7c0..b1e662cb 100644 --- a/feature/ranking/src/main/java/com/stackknowledge/ranking/navigation/RankingNavigation.kt +++ b/feature/ranking/src/main/java/com/stackknowledge/ranking/navigation/RankingNavigation.kt @@ -6,7 +6,7 @@ import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.stackknowledge.ranking.RankingRoute import com.stackknowledge.ranking.TeacherRankingRoute -import enumdatatype.Authority +import enumdata.Authority const val rankingRoute = "ranking_route" const val teacherRankingRoute = "teacher_ranking_route" diff --git a/feature/score-mission/src/main/java/com/stackknowledge/score_mission/GradingAnswerScreen.kt b/feature/score-mission/src/main/java/com/stackknowledge/score_mission/GradingAnswerScreen.kt index 97bdddde..daa39e8f 100644 --- a/feature/score-mission/src/main/java/com/stackknowledge/score_mission/GradingAnswerScreen.kt +++ b/feature/score-mission/src/main/java/com/stackknowledge/score_mission/GradingAnswerScreen.kt @@ -8,7 +8,6 @@ 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.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -19,7 +18,6 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -35,8 +33,7 @@ import com.stackknowledge.score_mission.component.SolvedMissionAnswer import com.stackknowledge.score_mission.component.SolvedMissionTitle import com.stackknowledge.score_mission.viewmodel.ScoreMissionViewModel import com.stackknowledge.score_mission.viewmodel.uistate.DetailScoreMissionUiState -import com.stackknowledge.score_mission.viewmodel.uistate.ScoreMissionUiState -import enumdatatype.Authority +import enumdata.Authority import remote.request.user.ScoreRequestModel @Composable diff --git a/feature/score-mission/src/main/java/com/stackknowledge/score_mission/SolvedMissionScreen.kt b/feature/score-mission/src/main/java/com/stackknowledge/score_mission/SolvedMissionScreen.kt index 17f18ee6..577736fd 100644 --- a/feature/score-mission/src/main/java/com/stackknowledge/score_mission/SolvedMissionScreen.kt +++ b/feature/score-mission/src/main/java/com/stackknowledge/score_mission/SolvedMissionScreen.kt @@ -22,7 +22,7 @@ import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.score_mission.component.SolvedMissionList import com.stackknowledge.score_mission.viewmodel.ScoreMissionViewModel import com.stackknowledge.score_mission.viewmodel.uistate.GetScoreMissionListUiState -import enumdatatype.Authority +import enumdata.Authority @Composable internal fun SolvedMissionRoute( diff --git a/feature/score-mission/src/main/java/com/stackknowledge/score_mission/navigation/ScoreMissionNavigation.kt b/feature/score-mission/src/main/java/com/stackknowledge/score_mission/navigation/ScoreMissionNavigation.kt index c77dfb66..7da44b41 100644 --- a/feature/score-mission/src/main/java/com/stackknowledge/score_mission/navigation/ScoreMissionNavigation.kt +++ b/feature/score-mission/src/main/java/com/stackknowledge/score_mission/navigation/ScoreMissionNavigation.kt @@ -6,7 +6,7 @@ import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.stackknowledge.score_mission.GradingAnswerRoute import com.stackknowledge.score_mission.SolvedMissionRoute -import enumdatatype.Authority +import enumdata.Authority const val gradingAnswerRoute = "grading_answer_route" const val solvedMissionRoute = "solved_mission_route" diff --git a/feature/shop/src/main/java/com/stackknowledge/shop/ShopScreen.kt b/feature/shop/src/main/java/com/stackknowledge/shop/ShopScreen.kt index da5c822e..e148d8b8 100644 --- a/feature/shop/src/main/java/com/stackknowledge/shop/ShopScreen.kt +++ b/feature/shop/src/main/java/com/stackknowledge/shop/ShopScreen.kt @@ -27,12 +27,12 @@ import com.stackknowledge.design_system.component.topbar.StackKnowledgeTopBar import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.shop.component.CurrentMileage import com.stackknowledge.shop.component.GoodsList +import enumdata.Authority import com.stackknowledge.shop.data.SelectedItemData import com.stackknowledge.shop.viewmodel.ItemViewModel import com.stackknowledge.shop.viewmodel.OrderViewModel import com.stackknowledge.shop.viewmodel.uistate.GetItemUiState import com.stackknowledge.shop.viewmodel.uistate.GetMyInformationUiState -import enumdatatype.Authority import remote.request.order.OrderRequestModel import remote.response.item.GetItemResponseModel diff --git a/feature/shop/src/main/java/com/stackknowledge/shop/TeacherShopScreen.kt b/feature/shop/src/main/java/com/stackknowledge/shop/TeacherShopScreen.kt index fef25e80..703b8987 100644 --- a/feature/shop/src/main/java/com/stackknowledge/shop/TeacherShopScreen.kt +++ b/feature/shop/src/main/java/com/stackknowledge/shop/TeacherShopScreen.kt @@ -23,9 +23,9 @@ import com.stackknowledge.design_system.component.dialog.StackKnowledgeDialog import com.stackknowledge.design_system.component.topbar.StackKnowledgeTopBar import com.stackknowledge.design_system.theme.StackKnowledgeAndroidTheme import com.stackknowledge.shop.component.OrderedGoodsList +import enumdata.Authority import com.stackknowledge.shop.viewmodel.OrderViewModel import com.stackknowledge.shop.viewmodel.uistate.GetOrderListUiState -import enumdatatype.Authority import remote.request.order.ChangeOrderStatusRequestModel @Composable diff --git a/feature/shop/src/main/java/com/stackknowledge/shop/navigation/ShopNavigation.kt b/feature/shop/src/main/java/com/stackknowledge/shop/navigation/ShopNavigation.kt index 49b811d8..158a12db 100644 --- a/feature/shop/src/main/java/com/stackknowledge/shop/navigation/ShopNavigation.kt +++ b/feature/shop/src/main/java/com/stackknowledge/shop/navigation/ShopNavigation.kt @@ -6,7 +6,7 @@ import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.stackknowledge.shop.ShopRoute import com.stackknowledge.shop.TeacherShopRoute -import enumdatatype.Authority +import enumdata.Authority const val shopRoute = "shop_route" const val teacherShopRoute = "teacher_shop_route" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d98f8063..55ebeea2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ androidxTestRunner = "1.5.2" androidx-test-ext-junit = "1.1.5" androidxWindowManager = "1.2.0" coil = "2.4.0" +firebase-auth = "22.0.0" converter-moshi = "2.9.0" firebaseBom = "31.2.0" firebaseCrashlyticsPlugin = "2.9.2" @@ -39,7 +40,6 @@ kotlinxSerializationJson = "1.5.1" ksp = "1.8.10-1.0.9" lint = "31.2.1" material = "1.2.0-alpha02" -moshi = "1.15.0" okhttp = "4.11.0" protobuf = "3.24.0" protobufPlugin = "0.9.4" @@ -48,6 +48,9 @@ retrofitKotlinxSerializableJson = "1.0.0" room = "2.6.1" org-jetbrains-kotlin-android = "1.8.10" lifecycle-runtime-ktx = "2.6.2" +google-services = "4.4.2" +play-services-auth = "20.7.0" +moshi = "1.15.0" [libraries] #Define Library @@ -69,6 +72,7 @@ androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", ve androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } +androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } @@ -96,6 +100,7 @@ kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" } moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" } +moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp"} okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } @@ -107,6 +112,10 @@ retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "re room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +google-services = { group = "com.google.gms", name = "google-services", version.ref = "google-services" } +firebase-auth = { group = "com.google", name = "firebase-auth", version.ref = "firebase-auth" } +firebase-bom = { group = "com.google", name = "firebase-bom", version.ref = "firebaseBom" } +play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" } #Define Dependeicies used in build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }