diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..77c75f79 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_function_naming_ignore_when_annotated_with = Composable, Test diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ca22c10d..f7a94a9f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,11 @@ ## 관련 이슈번호 +
close # ## 작업 사항 +
## 기타 사항 +
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4884378c..811a8b4b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -7,7 +7,7 @@ on: jobs: build: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v3 @@ -32,8 +32,15 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Access Google Client Id + env: + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + run: | + echo "GOOGLE_CLIENT_ID=\"$GOOGLE_CLIENT_ID\"" >> local.properties + - name: Run test run: ./gradlew test --parallel - name: Run ktlint run: ./gradlew ktlintCheck + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd3326e5..27881a38 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,3 +18,13 @@ android { } } +dependencies { + implementation(libs.androidx.core.splashscreen) + implementation(project(":feature:login")) + implementation(project(":core:interceptor")) + implementation(project(":core:data")) + implementation(project(":core:network")) + implementation(project(":core:datastore")) + implementation(project(":core:domain")) + implementation(project(":core:designsystem")) +} diff --git a/app/src/androidTest/java/com/withpeace/withpeace/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/withpeace/withpeace/ExampleInstrumentedTest.kt index a855b0aa..fabc1c37 100644 --- a/app/src/androidTest/java/com/withpeace/withpeace/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/withpeace/withpeace/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.withpeace.withpeace -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7591e8b8..868b88e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.withpeace.withpeace"> + + + android:theme="@style/Theme.Withpeace.Starting"> @@ -26,4 +29,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt index d0234cf5..df59ed52 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt @@ -1,48 +1,46 @@ package com.withpeace.withpeace import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.withpeace.withpeace.ui.theme.WithpeaceTheme +import androidx.activity.viewModels +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.feature.login.navigation.LOGIN_ROUTE import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : ComponentActivity() { + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val splashScreen = installSplashScreen() + splashScreen.setKeepOnScreenCondition { true } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.isLogin.collect { isLogin -> + if(isLogin) { + Log.d("covy","Main 화면으로 이동") + } else { + composeStart(LOGIN_ROUTE) + } + delay(2000L) + splashScreen.setKeepOnScreenCondition { false } + } + } + } + } + private fun ComponentActivity.composeStart(startDestination: String) { setContent { WithpeaceTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Android") - } + WithpeaceApp(startDestination = startDestination) } } } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - WithpeaceTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt b/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt new file mode 100644 index 00000000..e9145a9a --- /dev/null +++ b/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt @@ -0,0 +1,31 @@ +package com.withpeace.withpeace + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.withpeace.withpeace.core.domain.repository.TokenRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + private val tokenRepository: TokenRepository +) : ViewModel() { + private val _isLogin: MutableSharedFlow = MutableSharedFlow() + val isLogin = _isLogin.asSharedFlow() + + init { + viewModelScope.launch { + val token = tokenRepository.getAccessToken().firstOrNull() + if(token==null) { + _isLogin.emit(false) + } else { + _isLogin.emit(true) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/WithPeaceApplication.kt b/app/src/main/java/com/withpeace/withpeace/WithPeaceApplication.kt index cd59d3f1..8390979d 100644 --- a/app/src/main/java/com/withpeace/withpeace/WithPeaceApplication.kt +++ b/app/src/main/java/com/withpeace/withpeace/WithPeaceApplication.kt @@ -4,5 +4,5 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class WithPeaceApplication: Application() { +class WithPeaceApplication : Application() { } \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt new file mode 100644 index 00000000..c6ffc1b4 --- /dev/null +++ b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt @@ -0,0 +1,34 @@ +package com.withpeace.withpeace + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.withpeace.withpeace.navigation.WithpeaceNavHost +import kotlinx.coroutines.launch + + +@Composable +fun WithpeaceApp( + startDestination: String +) { + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + fun showSnackBar(message: String) = coroutineScope.launch { + snackBarHostState.showSnackbar(message) + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { SnackbarHost(snackBarHostState) }, + ) { + WithpeaceNavHost( + onShowSnackBar = ::showSnackBar, + ) + it + } +} \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt new file mode 100644 index 00000000..b4c6c9d4 --- /dev/null +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -0,0 +1,26 @@ +package com.withpeace.withpeace.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.withpeace.withpeace.feature.login.navigation.LOGIN_ROUTE +import com.withpeace.withpeace.feature.login.navigation.loginNavGraph + + +@Composable +fun WithpeaceNavHost( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + startDestination: String = LOGIN_ROUTE, + onShowSnackBar: (message: String) -> Unit, +) { + NavHost( + modifier = modifier, + navController = navController, + startDestination = startDestination, + ) { + loginNavGraph(onShowSnackBar = onShowSnackBar) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/ui/theme/Color.kt b/app/src/main/java/com/withpeace/withpeace/ui/theme/Color.kt deleted file mode 100644 index da096639..00000000 --- a/app/src/main/java/com/withpeace/withpeace/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.withpeace.withpeace.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/ui/theme/Theme.kt b/app/src/main/java/com/withpeace/withpeace/ui/theme/Theme.kt deleted file mode 100644 index 278371c2..00000000 --- a/app/src/main/java/com/withpeace/withpeace/ui/theme/Theme.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.withpeace.withpeace.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun WithpeaceTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/ui/theme/Type.kt b/app/src/main/java/com/withpeace/withpeace/ui/theme/Type.kt deleted file mode 100644 index bf02778e..00000000 --- a/app/src/main/java/com/withpeace/withpeace/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.withpeace.withpeace.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 8bb25279..d31100cb 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,21 @@ - + + diff --git a/app/src/test/java/com/withpeace/withpeace/ExampleUnitTest.kt b/app/src/test/java/com/withpeace/withpeace/ExampleUnitTest.kt index 7386af31..944b793d 100644 --- a/app/src/test/java/com/withpeace/withpeace/ExampleUnitTest.kt +++ b/app/src/test/java/com/withpeace/withpeace/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.withpeace.withpeace +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/build-logic/src/main/kotlin/convention.android.base.gradle.kts b/build-logic/src/main/kotlin/convention.android.base.gradle.kts index 8c406ca4..ff1cd935 100644 --- a/build-logic/src/main/kotlin/convention.android.base.gradle.kts +++ b/build-logic/src/main/kotlin/convention.android.base.gradle.kts @@ -32,9 +32,12 @@ android { excludes += "/META-INF/*" } } + buildFeatures { + buildConfig = true + } } -dependencies{ +dependencies { "androidTestImplementation"(libs.findLibrary("androidx.test.ext").get()) "androidTestImplementation"(libs.findLibrary("androidx-test-espresso-core").get()) "androidTestImplementation"(libs.findLibrary("junit4").get()) diff --git a/build-logic/src/main/kotlin/convention.feature.gradle.kts b/build-logic/src/main/kotlin/convention.feature.gradle.kts index 551b7340..756f561f 100644 --- a/build-logic/src/main/kotlin/convention.feature.gradle.kts +++ b/build-logic/src/main/kotlin/convention.feature.gradle.kts @@ -6,3 +6,9 @@ plugins { id("convention.android.compose") id("convention.android.hilt") } + +dependencies{ + implementation(project(":core:data")) + implementation(project(":core:domain")) + implementation(project(":core:designsystem")) +} \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 5ea0b847..0332be59 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -12,5 +12,6 @@ android { dependencies { implementation(project(":core:network")) implementation(project(":core:domain")) + implementation(project(":core:datastore")) implementation(libs.skydoves.sandwich) } diff --git a/core/data/src/androidTest/java/com/withpeace/withpeace/core/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/withpeace/withpeace/core/data/ExampleInstrumentedTest.kt index b3b26a3a..410df2d5 100644 --- a/core/data/src/androidTest/java/com/withpeace/withpeace/core/data/ExampleInstrumentedTest.kt +++ b/core/data/src/androidTest/java/com/withpeace/withpeace/core/data/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.withpeace.withpeace.core.data -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml index a5918e68..44008a43 100644 --- a/core/data/src/main/AndroidManifest.xml +++ b/core/data/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/di/RepositoryModule.kt new file mode 100644 index 00000000..9d9305a0 --- /dev/null +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/di/RepositoryModule.kt @@ -0,0 +1,18 @@ +package com.withpeace.withpeace.core.data.di + +import com.withpeace.withpeace.core.data.repository.DefaultTokenRepository +import com.withpeace.withpeace.core.domain.repository.TokenRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface RepositoryModule { + + @Binds + @Singleton + fun bindsTokenRepository(defaultTokenRepository: DefaultTokenRepository): TokenRepository +} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/TokenMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/TokenMapper.kt index e7d7c9c2..b95516fa 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/TokenMapper.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/TokenMapper.kt @@ -6,6 +6,6 @@ import com.withpeace.withpeace.core.network.di.response.TokenResponse fun TokenResponse.toDomain(): Token { return Token( accessToken = accessToken, - refreshToken = refreshToken + refreshToken = refreshToken, ) } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultTokenRepository.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultTokenRepository.kt index a443f048..7fa7acb8 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultTokenRepository.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultTokenRepository.kt @@ -1,29 +1,81 @@ package com.withpeace.withpeace.core.data.repository -import com.skydoves.sandwich.message +import com.skydoves.sandwich.messageOrNull import com.skydoves.sandwich.suspendMapSuccess -import com.skydoves.sandwich.suspendOnError +import com.skydoves.sandwich.suspendOnFailure +import com.skydoves.sandwich.suspendOnSuccess import com.withpeace.withpeace.core.data.mapper.toDomain +import com.withpeace.withpeace.core.datastore.dataStore.TokenPreferenceDataSource import com.withpeace.withpeace.core.domain.model.Token import com.withpeace.withpeace.core.domain.repository.TokenRepository +import com.withpeace.withpeace.core.network.di.request.SignUpRequest import com.withpeace.withpeace.core.network.di.service.AuthService +import com.withpeace.withpeace.core.network.di.service.LoginService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import javax.inject.Inject - -class DefaultTokenRepository @Inject constructor( - private val authService: AuthService +class DefaultTokenRepository +@Inject +constructor( + private val tokenPreferenceDataSource: TokenPreferenceDataSource, + private val loginService: LoginService, + private val authService: AuthService, ) : TokenRepository { + override fun getAccessToken(): Flow { + return tokenPreferenceDataSource.accessToken + } + + override fun getRefreshToken(): Flow { + return tokenPreferenceDataSource.refreshToken + } + + override suspend fun updateAccessToken(accessToken: String) { + tokenPreferenceDataSource.updateAccessToken(accessToken) + } + + override suspend fun updateRefreshToken(refreshToken: String) { + tokenPreferenceDataSource.updateRefreshToken(refreshToken) + } - override fun googleLogin(onError: (String?) -> Unit): Flow = flow { - authService.googleLogin() - .suspendMapSuccess { - emit(data.toDomain()) - }.suspendOnError { - onError(message()) - } + override suspend fun signUp( + onError: (String?) -> Unit, + email: String, + nickname: String, + deviceToken: String?, + ): Flow = flow { + authService.signUp( + SignUpRequest( + email = email, + nickname = nickname, + deviceToken = deviceToken, + ), + ).suspendMapSuccess { + emit(data.toDomain()) + }.suspendOnFailure { + onError(messageOrNull) + } }.flowOn(Dispatchers.IO) -} \ No newline at end of file + + + override fun googleLogin( + idToken: String, + onError: (String?) -> Unit, + ): Flow = + flow { + loginService.googleLogin(AUTHORIZATION_FORMAT.format(idToken)) + .suspendMapSuccess { + emit(data.toDomain()) + updateAccessToken(data.accessToken) + updateRefreshToken(data.refreshToken) + }.suspendOnFailure { + onError(messageOrNull) + } + }.flowOn(Dispatchers.IO) + + companion object { + private const val AUTHORIZATION_FORMAT = "Bearer %s" + } +} diff --git a/core/data/src/test/java/com/withpeace/withpeace/core/data/ExampleUnitTest.kt b/core/data/src/test/java/com/withpeace/withpeace/core/data/ExampleUnitTest.kt index 61be3bed..2dce1955 100644 --- a/core/data/src/test/java/com/withpeace/withpeace/core/data/ExampleUnitTest.kt +++ b/core/data/src/test/java/com/withpeace/withpeace/core/data/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.withpeace.withpeace.core.data +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/core/datastore/.gitignore b/core/datastore/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/datastore/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 00000000..6b784dd2 --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("com.android.library") + id("convention.android.base") + id("convention.android.hilt") +} + +android { + namespace = "com.withpeace.withpeace.core.datastore" +} + +dependencies { + implementation(libs.androidx.datastore) +} \ No newline at end of file diff --git a/core/datastore/consumer-rules.pro b/core/datastore/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/datastore/proguard-rules.pro b/core/datastore/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/datastore/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/google-login/src/androidTest/java/com/woosuk/google_login/ExampleInstrumentedTest.kt b/core/datastore/src/androidTest/java/com/withpeace/withpeace/core/datastore/ExampleInstrumentedTest.kt similarity index 80% rename from google-login/src/androidTest/java/com/woosuk/google_login/ExampleInstrumentedTest.kt rename to core/datastore/src/androidTest/java/com/withpeace/withpeace/core/datastore/ExampleInstrumentedTest.kt index f7fbd359..1e831671 100644 --- a/google-login/src/androidTest/java/com/woosuk/google_login/ExampleInstrumentedTest.kt +++ b/core/datastore/src/androidTest/java/com/withpeace/withpeace/core/datastore/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ -package com.woosuk.google_login +package com.withpeace.withpeace.core.datastore -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -19,6 +17,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.woosuk.google_login.test", appContext.packageName) + assertEquals("com.withpeace.withpeace.core.datastore.test", appContext.packageName) } -} +} \ No newline at end of file diff --git a/core/datastore/src/main/AndroidManifest.xml b/core/datastore/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44008a43 --- /dev/null +++ b/core/datastore/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/dataStore/DefaultTokenPreferenceDataSource.kt b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/dataStore/DefaultTokenPreferenceDataSource.kt new file mode 100644 index 00000000..6b5cca17 --- /dev/null +++ b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/dataStore/DefaultTokenPreferenceDataSource.kt @@ -0,0 +1,40 @@ +package com.withpeace.withpeace.core.datastore.dataStore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Named + +class DefaultTokenPreferenceDataSource @Inject constructor( + @Named("auth") private val dataStore: DataStore, +) : TokenPreferenceDataSource { + + override val accessToken: Flow = dataStore.data.map { preferences -> + preferences[ACCESS_TOKEN] + } + + override val refreshToken: Flow = dataStore.data.map { preferences -> + preferences[REFRESH_TOKEN] + } + + override suspend fun updateAccessToken(accessToken: String) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN] = accessToken + } + } + + override suspend fun updateRefreshToken(refreshToken: String) { + dataStore.edit { preferences -> + preferences[REFRESH_TOKEN] = refreshToken + } + } + + companion object { + private val ACCESS_TOKEN = stringPreferencesKey("ACCESS_TOKEN") + private val REFRESH_TOKEN = stringPreferencesKey("REFRESH_TOKEN") + } +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/dataStore/TokenPreferenceDataSource.kt b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/dataStore/TokenPreferenceDataSource.kt new file mode 100644 index 00000000..7d5df2be --- /dev/null +++ b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/dataStore/TokenPreferenceDataSource.kt @@ -0,0 +1,14 @@ +package com.withpeace.withpeace.core.datastore.dataStore + +import kotlinx.coroutines.flow.Flow + +interface TokenPreferenceDataSource { + + val accessToken: Flow + + val refreshToken: Flow + + suspend fun updateRefreshToken(refreshToken: String) + + suspend fun updateAccessToken(accessToken: String) +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/di/DataStoreModule.kt new file mode 100644 index 00000000..8215224f --- /dev/null +++ b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/di/DataStoreModule.kt @@ -0,0 +1,29 @@ +package com.withpeace.withpeace.core.datastore.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + + private const val AUTH_DATASTORE_NAME = "AUTH_PREFERENCES" + + private val Context.authDataStore: DataStore by preferencesDataStore(name = AUTH_DATASTORE_NAME) + + @Provides + @Singleton + @Named("auth") + fun providesTokenDataStore( + @ApplicationContext context: Context, + ): DataStore = context.authDataStore +} \ No newline at end of file diff --git a/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/di/PreferenceDataSourceModule.kt b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/di/PreferenceDataSourceModule.kt new file mode 100644 index 00000000..e0beaa82 --- /dev/null +++ b/core/datastore/src/main/java/com/withpeace/withpeace/core/datastore/di/PreferenceDataSourceModule.kt @@ -0,0 +1,20 @@ +package com.withpeace.withpeace.core.datastore.di + +import com.withpeace.withpeace.core.datastore.dataStore.DefaultTokenPreferenceDataSource +import com.withpeace.withpeace.core.datastore.dataStore.TokenPreferenceDataSource +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface PreferenceDataSourceModule { + + @Binds + @Singleton + fun bindsTokenPreferenceDataSource( + defaultTokenPreferenceDataSource: DefaultTokenPreferenceDataSource, + ): TokenPreferenceDataSource +} \ No newline at end of file diff --git a/google-login/src/test/java/com/woosuk/google_login/ExampleUnitTest.kt b/core/datastore/src/test/java/com/withpeace/withpeace/core/datastore/ExampleUnitTest.kt similarity index 77% rename from google-login/src/test/java/com/woosuk/google_login/ExampleUnitTest.kt rename to core/datastore/src/test/java/com/withpeace/withpeace/core/datastore/ExampleUnitTest.kt index a93ea3c8..c98a0772 100644 --- a/google-login/src/test/java/com/woosuk/google_login/ExampleUnitTest.kt +++ b/core/datastore/src/test/java/com/withpeace/withpeace/core/datastore/ExampleUnitTest.kt @@ -1,9 +1,8 @@ -package com.woosuk.google_login +package com.withpeace.withpeace.core.datastore +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} +} \ No newline at end of file diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 00000000..d518a6ce --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("com.android.library") + id("convention.android.base") + id("convention.android.compose") +} + +android { + namespace = "com.withpeace.withpeace.core.designsystem" +} diff --git a/core/designsystem/consumer-rules.pro b/core/designsystem/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/designsystem/proguard-rules.pro b/core/designsystem/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/designsystem/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/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt new file mode 100644 index 00000000..a6fe57cb --- /dev/null +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt @@ -0,0 +1,41 @@ +package com.withpeace.withpeace.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val mainpink = Color(0xFFFEA0A1) +val subPink = Color(0xFFF5D6DB) +val subApricot = Color(0xFFFED9C9) +val subBlue = Color(0xFFDFF2F9) + +val systemBlack = Color(0xFF212529) +val systemWhite = Color.White +val systemGray1 = Color(0xFF3D3D3D) +val systemGray2 = Color(0xFFA7A7A7) +val systemGray3 = Color(0xFFECECEF) +val systemError = Color(0xFFF0474B) +val systemSuccess = Color(0xFF3BD569) + +data class WithPeaceColor( + val MainPink: Color = mainpink, + val SubPink: Color = subPink, + val SubApricot: Color = subApricot, + val SubBlue: Color = subBlue, + val SystemBlack: Color = systemBlack, + val SystemWhite: Color = systemWhite, + val SystemGray1: Color = systemGray1, + val SystemGray2: Color = systemGray2, + val SystemGray3: Color = systemGray3, + val SystemError: Color = systemError, + val SystemSuccess: Color = systemSuccess, +) + +val lightColor = WithPeaceColor() +val darkColor = WithPeaceColor() diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Padding.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Padding.kt new file mode 100644 index 00000000..6323cee1 --- /dev/null +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Padding.kt @@ -0,0 +1,9 @@ +package com.withpeace.withpeace.core.designsystem.theme + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class WithPeacePadding( + val BasicHorizontalPadding: Dp = 24.dp, + val BasicContentPadding: Dp = 8.dp, +) diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Theme.kt new file mode 100644 index 00000000..c6dfdb62 --- /dev/null +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Theme.kt @@ -0,0 +1,50 @@ +package com.withpeace.withpeace.core.designsystem.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf + +val LocalCustomColors = + staticCompositionLocalOf { + WithPeaceColor() + } + +val LocalCustomTypography = + staticCompositionLocalOf { + WithPeaceTypography() + } +val LocalCustomPadding = + staticCompositionLocalOf { + WithPeacePadding() + } + +@Composable +fun WithpeaceTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = + when { + darkTheme -> darkColor + else -> lightColor + } + CompositionLocalProvider( + LocalCustomColors provides colorScheme, + LocalCustomTypography provides WithPeaceTypography(), + LocalCustomPadding provides WithPeacePadding(), + content = content, + ) +} + +object WithpeaceTheme { + val colors: WithPeaceColor + @Composable + get() = LocalCustomColors.current + val typography: WithPeaceTypography + @Composable + get() = LocalCustomTypography.current + val padding: WithPeacePadding + @Composable + get() = LocalCustomPadding.current +} diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt new file mode 100644 index 00000000..4e0df278 --- /dev/null +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt @@ -0,0 +1,135 @@ +package com.withpeace.withpeace.core.designsystem.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.withpeace.withpeace.core.designsystem.R + +// Set of Material typography styles to start with +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ + ) + +val NotoSansFont = + FontFamily( + Font( + resId = R.font.notosans_kr_medium, + weight = FontWeight.Bold, + ), + ) + +val PretendardFont = + FontFamily( + Font( + resId = R.font.pretendard_bold, + weight = FontWeight.Bold, + ), + Font( + resId = R.font.pretendard_regular, + weight = FontWeight.Normal, + ), + Font( + resId = R.font.pretendard_extra_bold, + weight = FontWeight.ExtraBold, + ), + Font( + resId = R.font.pretendard_extra_light, + weight = FontWeight.ExtraLight, + ), + Font( + resId = R.font.pretendard_extra_light, + weight = FontWeight.Light, + ), + Font( + resId = R.font.pretendard_medium, + weight = FontWeight.Medium, + ), + Font( + resId = R.font.pretendard_semi_bold, + weight = FontWeight.SemiBold, + ), + Font( + resId = R.font.pretendard_thin, + weight = FontWeight.Thin, + ), + ) + +@Immutable +data class WithPeaceTypography( + val notoSans: TextStyle = + TextStyle( + fontFamily = NotoSansFont, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 23.17.sp, + ), + val heading: TextStyle = + TextStyle( + fontFamily = PretendardFont, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 33.6.sp, + letterSpacing = (-0.096).sp, + ), + val title1: TextStyle = + TextStyle( + fontFamily = PretendardFont, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + lineHeight = 28.sp, + letterSpacing = (-0.08).sp, + ), + val title2: TextStyle = + TextStyle( + fontFamily = PretendardFont, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + lineHeight = 25.2.sp, + letterSpacing = (-0.4).sp, + ), + val body: TextStyle = + TextStyle( + fontFamily = PretendardFont, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 22.4.sp, + letterSpacing = (-0.4).sp, + ), + val caption: TextStyle = + TextStyle( + fontFamily = PretendardFont, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 19.6.sp, + letterSpacing = (-0.4).sp, + ), +) diff --git a/core/designsystem/src/main/res/font/notosans_kr_medium.ttf b/core/designsystem/src/main/res/font/notosans_kr_medium.ttf new file mode 100644 index 00000000..5311c8a3 Binary files /dev/null and b/core/designsystem/src/main/res/font/notosans_kr_medium.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_black.ttf b/core/designsystem/src/main/res/font/pretendard_black.ttf new file mode 100644 index 00000000..d0c1db81 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_black.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_bold.ttf b/core/designsystem/src/main/res/font/pretendard_bold.ttf new file mode 100644 index 00000000..fb07fc65 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_bold.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_extra_bold.ttf b/core/designsystem/src/main/res/font/pretendard_extra_bold.ttf new file mode 100644 index 00000000..9d5fe072 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_extra_bold.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_extra_light.ttf b/core/designsystem/src/main/res/font/pretendard_extra_light.ttf new file mode 100644 index 00000000..09e65428 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_extra_light.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_light.ttf b/core/designsystem/src/main/res/font/pretendard_light.ttf new file mode 100644 index 00000000..2e8541d6 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_light.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_medium.ttf b/core/designsystem/src/main/res/font/pretendard_medium.ttf new file mode 100644 index 00000000..1db67c68 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_medium.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_regular.ttf b/core/designsystem/src/main/res/font/pretendard_regular.ttf new file mode 100644 index 00000000..01147e99 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_regular.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_semi_bold.ttf b/core/designsystem/src/main/res/font/pretendard_semi_bold.ttf new file mode 100644 index 00000000..9f2690f0 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_semi_bold.ttf differ diff --git a/core/designsystem/src/main/res/font/pretendard_thin.ttf b/core/designsystem/src/main/res/font/pretendard_thin.ttf new file mode 100644 index 00000000..fe9825f1 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_thin.ttf differ diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/Token.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/Token.kt index 8fddfc8b..f8ca3553 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/Token.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/Token.kt @@ -2,5 +2,5 @@ package com.withpeace.withpeace.core.domain.model data class Token( val accessToken: String, - val refreshToken: String + val refreshToken: String, ) \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/TokenRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/TokenRepository.kt index fef27c35..c2f14965 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/TokenRepository.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/TokenRepository.kt @@ -5,5 +5,23 @@ import kotlinx.coroutines.flow.Flow interface TokenRepository { - fun googleLogin(onError: (message: String?) -> Unit): Flow + fun getAccessToken(): Flow + + fun getRefreshToken(): Flow + + suspend fun updateAccessToken(accessToken: String) + + suspend fun updateRefreshToken(refreshToken: String) + + suspend fun signUp( + onError: (message: String?) -> Unit, + email: String, + nickname: String, + deviceToken: String?, + ): Flow + + fun googleLogin( + idToken: String, + onError: (message: String?) -> Unit, + ): Flow } \ No newline at end of file diff --git a/core/interceptor/.gitignore b/core/interceptor/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/interceptor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/interceptor/build.gradle.kts b/core/interceptor/build.gradle.kts new file mode 100644 index 00000000..1967e9b0 --- /dev/null +++ b/core/interceptor/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("com.android.library") + id("convention.android.base") + id("convention.android.hilt") +} + +android { + namespace = "com.withpeace.withpeace.core.interceptor" +} + +dependencies { + implementation(libs.kotlinx.serialization.json) + implementation(libs.retrofit.core) + implementation(libs.okhttp.logging) + implementation(project(":core:datastore")) + implementation(project(":core:network")) +} \ No newline at end of file diff --git a/core/interceptor/consumer-rules.pro b/core/interceptor/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/interceptor/proguard-rules.pro b/core/interceptor/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/interceptor/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/interceptor/src/androidTest/java/com/withpeace/withpeace/core/interceptor/ExampleInstrumentedTest.kt b/core/interceptor/src/androidTest/java/com/withpeace/withpeace/core/interceptor/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..5d4149a9 --- /dev/null +++ b/core/interceptor/src/androidTest/java/com/withpeace/withpeace/core/interceptor/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.withpeace.withpeace.core.interceptor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * 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.withpeace.withpeace.core.interceptor.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/interceptor/src/main/AndroidManifest.xml b/core/interceptor/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44008a43 --- /dev/null +++ b/core/interceptor/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/AuthInterceptor.kt b/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/AuthInterceptor.kt new file mode 100644 index 00000000..305606f8 --- /dev/null +++ b/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/AuthInterceptor.kt @@ -0,0 +1,87 @@ +package com.withpeace.withpeace.core.interceptor + +import com.withpeace.withpeace.core.datastore.dataStore.TokenPreferenceDataSource +import com.withpeace.withpeace.core.network.di.response.TokenResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import javax.inject.Inject + +class AuthInterceptor @Inject constructor( + private val tokenPreferenceDataSource: TokenPreferenceDataSource, +) : Interceptor { + private val client = OkHttpClient.Builder().build() + + override fun intercept(chain: Interceptor.Chain): Response { + val accessToken = runBlocking { tokenPreferenceDataSource.accessToken.firstOrNull() } + val tokenAddedRequest = + chain.request() + .newBuilder() + .addHeader( + ACCESS_TOKEN_HEADER, + TOKEN_FORMAT.format(accessToken), + ).build() + var response = chain.proceed(tokenAddedRequest) + + if (response.code == 401) { + val refreshToken = runBlocking { tokenPreferenceDataSource.refreshToken.firstOrNull() } + if (refreshToken != null) { + runCatching { + refreshAccessToken(refreshToken) + }.onSuccess { tokenResponse -> + runBlocking { + tokenPreferenceDataSource.updateAccessToken(tokenResponse.accessToken) + tokenPreferenceDataSource.updateRefreshToken(tokenResponse.refreshToken) + } + response = + chain.proceed( + chain.request().newBuilder().addHeader( + ACCESS_TOKEN_HEADER, + TOKEN_FORMAT.format(tokenResponse.accessToken), + ).build(), + ) + } + } + } + return response + } + + private fun refreshAccessToken(refreshToken: String): TokenResponse { + val response: Response = + runBlocking { + withContext(Dispatchers.IO) { + client.newCall(createAccessTokenRefreshRequest(refreshToken)).execute() + } + } + if (response.isSuccessful) { + return response.toDto() + } + throw IllegalArgumentException() + } + + private fun createAccessTokenRefreshRequest(refreshToken: String): Request { + return Request.Builder() + .url(REFRESH_URL) + .addHeader(REFRESH_TOKEN_FORMAT, TOKEN_FORMAT.format(refreshToken)) + .build() + } + + private inline fun Response.toDto(): T { + body?.let { + return Json.decodeFromString(it.string()) + } ?: throw IllegalArgumentException() + } + + companion object { + private const val REFRESH_URL = "http://49.50.160.170:8080/api/v1/auth/refresh" + private const val REFRESH_TOKEN_FORMAT = "ReAuthorization" + private const val ACCESS_TOKEN_HEADER = "Authorization" + private const val TOKEN_FORMAT = "Bearer %s" + } +} diff --git a/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/InterceptorModule.kt b/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/InterceptorModule.kt new file mode 100644 index 00000000..f1d968a1 --- /dev/null +++ b/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/InterceptorModule.kt @@ -0,0 +1,25 @@ +package com.withpeace.withpeace.core.interceptor + +import android.content.Context +import com.withpeace.withpeace.core.datastore.dataStore.TokenPreferenceDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import okhttp3.Interceptor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object InterceptorModule { + + @Provides + @Singleton + fun provideHeaderInterceptor( + tokenPreferenceDataSource: TokenPreferenceDataSource, + ): Interceptor = + AuthInterceptor(tokenPreferenceDataSource) + + +} \ No newline at end of file diff --git a/core/interceptor/src/test/java/com/withpeace/withpeace/core/interceptor/ExampleUnitTest.kt b/core/interceptor/src/test/java/com/withpeace/withpeace/core/interceptor/ExampleUnitTest.kt new file mode 100644 index 00000000..03abd607 --- /dev/null +++ b/core/interceptor/src/test/java/com/withpeace/withpeace/core/interceptor/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.withpeace.withpeace.core.interceptor + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * 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/core/network/src/androidTest/java/com/withpeace/withpeace/core/network/ExampleInstrumentedTest.kt b/core/network/src/androidTest/java/com/withpeace/withpeace/core/network/ExampleInstrumentedTest.kt index 75fdeedb..dd396ff4 100644 --- a/core/network/src/androidTest/java/com/withpeace/withpeace/core/network/ExampleInstrumentedTest.kt +++ b/core/network/src/androidTest/java/com/withpeace/withpeace/core/network/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.withpeace.withpeace.core.network -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/core/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml index a5918e68..44008a43 100644 --- a/core/network/src/main/AndroidManifest.xml +++ b/core/network/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/AuthInterceptor.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/AuthInterceptor.kt new file mode 100644 index 00000000..e69de29b diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt similarity index 66% rename from core/network/src/main/java/com/withpeace/withpeace/core/network/di/NetworkModule.kt rename to core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt index e84b59a3..dae7fe46 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt @@ -1,4 +1,4 @@ -package com.withpeace.withpeace.core.network.di +package com.withpeace.withpeace.core.network.di.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.skydoves.sandwich.adapters.ApiResponseCallAdapterFactory @@ -7,11 +7,13 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Converter import retrofit2.Retrofit +import javax.inject.Named import javax.inject.Singleton @Module @@ -39,34 +41,45 @@ object NetworkModule { } } -// @Provides -// @Singleton -// fun provideHeaderInterceptor(chain: Interceptor.Chain) { -// val requestBuilder = chain.request().newBuilder() -// var apiKey = BuildConfig.X_RIOT_TOKEN -// requestBuilder.addHeader("X-Riot-Token", apiKey) -// chain.proceed(requestBuilder.build()) -// } - @Singleton @Provides - fun provideOkhttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient { + fun provideOkhttpClient( + authInterceptor: Interceptor, + httpLoggingInterceptor: HttpLoggingInterceptor, + ): OkHttpClient { return OkHttpClient.Builder().apply { - // addInterceptor(AccessTokenInterceptor) TODO("토큰 인터셉터 할당") + addInterceptor(authInterceptor) addInterceptor(httpLoggingInterceptor) }.build() } - + @Named("general") @Provides @Singleton - fun provideRetrofitClient( + fun provideTokenRetrofitClient( okHttpClient: OkHttpClient, - converterFactory: Converter.Factory + converterFactory: Converter.Factory, ): Retrofit { return Retrofit.Builder() .client(okHttpClient) - .baseUrl("https://asia.api.riotgames.com/") // TODO("BaseUrl 수정") + .baseUrl("http://49.50.160.170:8080/") + .addConverterFactory(converterFactory) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .build() + } + + + /** + * todo: 네이밍 수정 + */ + @Named("initial") + @Provides + @Singleton + fun provideRetrofitClient( + converterFactory: Converter.Factory, + ): Retrofit { + return Retrofit.Builder() + .baseUrl("http://49.50.160.170:8080/") .addConverterFactory(converterFactory) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) .build() diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt new file mode 100644 index 00000000..4c598414 --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt @@ -0,0 +1,26 @@ +package com.withpeace.withpeace.core.network.di.di + +import com.withpeace.withpeace.core.network.di.service.AuthService +import com.withpeace.withpeace.core.network.di.service.LoginService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + + @Provides + @Singleton + fun providesAuthService(@Named("general") retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) + + @Provides + @Singleton + fun providesLoginService(@Named("initial") retrofit: Retrofit): LoginService = + retrofit.create(LoginService::class.java) +} \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/request/SignUpRequest.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/request/SignUpRequest.kt index af72b92d..d69487fb 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/request/SignUpRequest.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/request/SignUpRequest.kt @@ -1,5 +1,8 @@ package com.withpeace.withpeace.core.network.di.request +import kotlinx.serialization.Serializable + +@Serializable data class SignUpRequest( val email: String, val nickname: String, diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/TokenResponse.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/TokenResponse.kt index 26350bd7..7f0c414f 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/TokenResponse.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/TokenResponse.kt @@ -1,5 +1,8 @@ package com.withpeace.withpeace.core.network.di.response +import kotlinx.serialization.Serializable + +@Serializable data class TokenResponse( val accessToken: String, val refreshToken: String, diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AuthService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AuthService.kt index 546e1ed0..fffa79ad 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AuthService.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AuthService.kt @@ -5,18 +5,18 @@ import com.withpeace.withpeace.core.network.di.request.SignUpRequest import com.withpeace.withpeace.core.network.di.response.BaseResponse import com.withpeace.withpeace.core.network.di.response.TokenResponse import retrofit2.http.Body +import retrofit2.http.Header import retrofit2.http.POST interface AuthService { - @POST("/api/v1/auth/google") - fun googleLogin(): ApiResponse> - @POST("/api/v1/auth/register") - fun signUp(@Body signUpRequest: SignUpRequest): ApiResponse> + suspend fun signUp( + @Body signUpRequest: SignUpRequest, + ): ApiResponse> @POST("/api/v1/auth/refresh") - fun refreshAccessToken(): ApiResponse> + suspend fun refreshAccessToken(): ApiResponse> @POST("/api/v1/auth/logout") - fun logout(): ApiResponse> + suspend fun logout(): ApiResponse> } \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/LoginService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/LoginService.kt new file mode 100644 index 00000000..deee313d --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/LoginService.kt @@ -0,0 +1,16 @@ +package com.withpeace.withpeace.core.network.di.service + +import com.skydoves.sandwich.ApiResponse +import com.withpeace.withpeace.core.network.di.response.BaseResponse +import com.withpeace.withpeace.core.network.di.response.TokenResponse +import retrofit2.http.Header +import retrofit2.http.POST + +interface LoginService { + + @POST("/api/v1/auth/google") + suspend fun googleLogin( + @Header("Authorization") + idToken: String, + ): ApiResponse> +} \ No newline at end of file diff --git a/core/network/src/test/java/com/withpeace/withpeace/core/network/ExampleUnitTest.kt b/core/network/src/test/java/com/withpeace/withpeace/core/network/ExampleUnitTest.kt index 9b8a7dd4..3a01bec8 100644 --- a/core/network/src/test/java/com/withpeace/withpeace/core/network/ExampleUnitTest.kt +++ b/core/network/src/test/java/com/withpeace/withpeace/core/network/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.withpeace.withpeace.core.network +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/feature/login/.gitignore b/feature/login/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/login/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts new file mode 100644 index 00000000..0167a052 --- /dev/null +++ b/feature/login/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("convention.feature") +} + +android { + namespace = "com.withpeace.withpeace.feature.login" +} + +dependencies { + implementation(project(":google-login")) +} diff --git a/feature/login/consumer-rules.pro b/feature/login/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/login/proguard-rules.pro b/feature/login/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/login/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/feature/login/src/main/AndroidManifest.xml b/feature/login/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7179a81c --- /dev/null +++ b/feature/login/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt new file mode 100644 index 00000000..dd2db9f9 --- /dev/null +++ b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt @@ -0,0 +1,137 @@ +package com.withpeace.withpeace.feature.login + +import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +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.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.googlelogin.GoogleLoginManager + +@Composable +fun LoginRoute( + viewModel: LoginViewModel = hiltViewModel(), + onShowSnackBar: (message: String) -> Unit, +) { + LoginScreen( + onGoogleLogin = viewModel::googleLogin, + ) + LaunchedEffect(key1 = null) { + viewModel.loginUiEvent.collect { uiEvent -> + when (uiEvent) { + is LoginUiEvent.SignUpSuccess -> { + onShowSnackBar("LoginRoute: 로그인 성공") + } + + is LoginUiEvent.SignUpFail -> { + onShowSnackBar("LoginRoute: 로그인 실패") + } + } + } + } +} + +@Composable +fun LoginScreen( + onGoogleLogin: (idToken: String) -> Unit = {}, +) { + val googleLoginManager = GoogleLoginManager(context = LocalContext.current) + val coroutineScope = rememberCoroutineScope() + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(152.dp)) + Image( + painter = painterResource(id = R.drawable.app_logo), + contentDescription = stringResource(R.string.app_logo_content_description), + ) + Spacer(modifier = Modifier.height(40.dp)) + Text( + style = WithpeaceTheme.typography.title1, + text = stringResource(R.string.welcome_to_withpeace), + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(R.string.welcome_introduction), + textAlign = TextAlign.Center, + ) + } + Button( + onClick = { + googleLoginManager.startLogin( + coroutineScope = coroutineScope, + onSuccessLogin = onGoogleLogin, + onFailLogin = { Log.e("woogi", "LoginScreen: 로그인 실패") }, + ) + }, + contentPadding = PaddingValues(0.dp), + modifier = Modifier + .padding( + bottom = 40.dp, + end = WithpeaceTheme.padding.BasicHorizontalPadding, + start = WithpeaceTheme.padding.BasicHorizontalPadding, + ) + .fillMaxWidth(), + border = BorderStroke(width = 1.dp, color = WithpeaceTheme.colors.SystemBlack), + colors = ButtonDefaults.buttonColors(containerColor = WithpeaceTheme.colors.SystemWhite), + shape = RoundedCornerShape(9.dp), + ) { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + Image( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterStart) + .size(24.dp), + painter = painterResource(id = R.drawable.img_google_logo), + contentDescription = stringResource(R.string.image_google_logo), + ) + Text( + modifier = Modifier.align(Alignment.Center), + color = WithpeaceTheme.colors.SystemBlack, + style = WithpeaceTheme.typography.notoSans, + text = stringResource(R.string.login_to_google), + ) + } + } + } +} + +@Preview(widthDp = 400, heightDp = 900, showBackground = true) +@Composable +fun LoginScreenPreview() { + LoginScreen() +} diff --git a/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginUiEvent.kt b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginUiEvent.kt new file mode 100644 index 00000000..536efdd5 --- /dev/null +++ b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginUiEvent.kt @@ -0,0 +1,8 @@ +package com.withpeace.withpeace.feature.login + +sealed interface LoginUiEvent { + + data object SignUpSuccess: LoginUiEvent + + data class SignUpFail(val message: String): LoginUiEvent +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginViewModel.kt new file mode 100644 index 00000000..1f56fea8 --- /dev/null +++ b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginViewModel.kt @@ -0,0 +1,55 @@ +package com.withpeace.withpeace.feature.login + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.withpeace.withpeace.core.domain.repository.TokenRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val tokenRepository: TokenRepository, +) : ViewModel() { + + private val _loginUiEvent: MutableSharedFlow = MutableSharedFlow() + val loginUiEvent = _loginUiEvent.asSharedFlow() + + fun googleLogin(idToken: String) { + viewModelScope.launch { + tokenRepository.googleLogin(idToken) { + Log.e("woogi", it ?: "메시지 없음") + launch { + _loginUiEvent.emit(LoginUiEvent.SignUpFail(it ?: "메시지 없음")) + } + }.collect { token -> + tokenRepository.updateAccessToken(token.accessToken) + tokenRepository.updateRefreshToken(token.refreshToken) + signUp() + } + } + } + + private fun signUp() { + viewModelScope.launch { + tokenRepository.signUp( + email = "abasdfasdf", + nickname = "haha", + deviceToken = null, + onError = { + launch { + _loginUiEvent.emit(LoginUiEvent.SignUpFail(it ?: "메시지 없음")) + } + } + ).collect { token -> + tokenRepository.updateAccessToken(token.accessToken) + tokenRepository.updateRefreshToken(token.refreshToken) + _loginUiEvent.emit(LoginUiEvent.SignUpSuccess) + } + } + } +} diff --git a/feature/login/src/main/java/com/withpeace/withpeace/feature/login/navigation/LoginNavigation.kt b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/navigation/LoginNavigation.kt new file mode 100644 index 00000000..499cd550 --- /dev/null +++ b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/navigation/LoginNavigation.kt @@ -0,0 +1,21 @@ +package com.withpeace.withpeace.feature.login.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.withpeace.withpeace.feature.login.LoginRoute + +const val LOGIN_ROUTE = "loginRoute" + +fun NavController.navigateLogin(navOptions: NavOptions? = null) { + navigate(LOGIN_ROUTE, navOptions) +} + +fun NavGraphBuilder.loginNavGraph( + onShowSnackBar: (message: String) -> Unit, +) { + composable(route = LOGIN_ROUTE) { + LoginRoute(onShowSnackBar = onShowSnackBar) + } +} \ No newline at end of file diff --git a/feature/login/src/main/res/drawable/app_logo.png b/feature/login/src/main/res/drawable/app_logo.png new file mode 100644 index 00000000..544764ca Binary files /dev/null and b/feature/login/src/main/res/drawable/app_logo.png differ diff --git a/feature/login/src/main/res/drawable/img_google_logo.png b/feature/login/src/main/res/drawable/img_google_logo.png new file mode 100644 index 00000000..5bae1042 Binary files /dev/null and b/feature/login/src/main/res/drawable/img_google_logo.png differ diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml new file mode 100644 index 00000000..d569aac8 --- /dev/null +++ b/feature/login/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + app logo + 위드피스에 오신 것을 환영합니다 + image_google_logo + Google로 로그인하기 + 1인 가구의 모든 것\n 유용한 정보를 함께 공유해보세요! + \ No newline at end of file diff --git a/google-login/build.gradle.kts b/google-login/build.gradle.kts index e5a9f73e..c16ef1fa 100644 --- a/google-login/build.gradle.kts +++ b/google-login/build.gradle.kts @@ -1,43 +1,30 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + +fun getLocalPropertyString(propertyKey: String): String { + return gradleLocalProperties(rootDir).getProperty(propertyKey) +} + plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + id("com.android.library") + id("convention.android.compose") + id("convention.android.hilt") + id("convention.coroutine") } android { - namespace = "com.woosuk.google_login" - compileSdk = 34 + namespace = "com.withpeace.withpeace.googlelogin" defaultConfig { - minSdk = 26 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" + buildConfigField( + "String", + "GOOGLE_CLIENT_ID", + getLocalPropertyString("GOOGLE_CLIENT_ID"), + ) } } dependencies { - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - testImplementation(libs.junit4) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.espresso.core) + implementation(libs.google.login) + implementation(libs.androidx.credentials) + implementation(libs.androidx.credentials.play.service) } diff --git a/google-login/src/main/AndroidManifest.xml b/google-login/src/main/AndroidManifest.xml index 8bdb7e14..c45e6dfd 100644 --- a/google-login/src/main/AndroidManifest.xml +++ b/google-login/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/google-login/src/main/kotlin/com/withpeace/withpeace/googlelogin/GoogleLoginManager.kt b/google-login/src/main/kotlin/com/withpeace/withpeace/googlelogin/GoogleLoginManager.kt new file mode 100644 index 00000000..5ad52d7b --- /dev/null +++ b/google-login/src/main/kotlin/com/withpeace/withpeace/googlelogin/GoogleLoginManager.kt @@ -0,0 +1,57 @@ +package com.withpeace.withpeace.googlelogin + +import android.content.Context +import androidx.credentials.Credential +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class GoogleLoginManager(val context: Context) { + private val credentialManager = CredentialManager.create(context) + + private val googleIdOption: GetSignInWithGoogleOption = + GetSignInWithGoogleOption.Builder(BuildConfig.GOOGLE_CLIENT_ID) + .build() + + private val credentialRequest = GetCredentialRequest(listOf(googleIdOption)) + + fun startLogin( + coroutineScope: CoroutineScope, + onSuccessLogin: (String) -> Unit, + onFailLogin: (String?) -> Unit, + ) { + coroutineScope.launch { + runCatching { + val result = credentialManager.getCredential(context, credentialRequest) + handleSignIn(result, onSuccessLogin, onFailLogin) + } + } + } + + private fun handleSignIn( + result: GetCredentialResponse, + onSuccessLogin: (String) -> Unit, + onFailLogin: (String?) -> Unit, + ) { + val credential = result.credential + if (credential.isCustomAndRightType()) { + runCatching { + GoogleIdTokenCredential.createFrom(credential.data) + }.onSuccess { + onSuccessLogin(it.idToken) + }.onFailure { + onFailLogin(it.toString()) + } + } else { + onFailLogin(null) + } + } + + private fun Credential.isCustomAndRightType() = + this is CustomCredential && type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1683179a..b620c3c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,10 @@ material = "1.11.0" material3Android = "1.2.0" multidex = "2.0.1" +google-login = "1.1.0" + +credential = "1.2.0" + [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } @@ -80,6 +84,9 @@ androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-t androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } androidx-compose-navigation-test = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxComposeNavigation" } +androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "credential" } +androidx-credentials-play-service = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credential" } + hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } @@ -132,6 +139,8 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } multidex = { group = "androidx.multidex", name = "multidex", version.ref = "multidex" } +google-login = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "google-login" } + # verify verify-detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index df827c44..f73b4e73 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,10 @@ dependencyResolutionManagement { rootProject.name = "withpeace" include(":app") include(":google-login") +include(":feature:login") include(":core:network") include(":core:data") include(":core:domain") +include(":core:datastore") +include(":core:designsystem") +include(":core:interceptor")