Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ jobs:
env:
DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }}
PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
run: |
echo "dev_base_url=$DEV_BASE_URL" >> local.properties
echo "prod_base_url=$PROD_BASE_URL" >> local.properties
echo "google_client_id=$GOOGLE_CLIENT_ID" >> local.properties

- name: Create google-services.json
env:
Expand Down
10 changes: 2 additions & 8 deletions core/datastore/src/main/java/com/twix/datastore/AuthConfigure.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,8 @@ import java.io.OutputStream

@Serializable
internal data class AuthConfigure(
val accessToken: String =
"eyJhbGciOiJIUzM4NCJ9." +
"eyJzdWIiOiIxIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTc3MDI0NzM0NCwiZXhwIjoxNzcwODUyMTQ0fQ." +
"67rDscm8BeayYFA1gfcEMliEdEh8-HTUyE5TwmAT8Ef8ZvtaWczxpMNZqI5htiek",
val refreshToken: String =
"eyJhbGciOiJIUzM4NCJ9." +
"eyJzdWIiOiIxIiwidHlwZSI6InJlZnJlc2giLCJpYXQiOjE3NzAyNDczNDQsImV4cCI6MTc3MDg1MjE0NH0." +
"zgUYdR6onyeY5EaH2_pWLs1rjNLf8m8ZeXsY7Cbk99a_2tzR0rDBZO_hdGTnorRL",
val accessToken: String = "",
val refreshToken: String = "",
)

internal object AuthConfigureSerializer : Serializer<AuthConfigure> {
Expand Down
22 changes: 22 additions & 0 deletions core/design-system/src/main/res/drawable/ic_google.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M15.028,7.842C15.028,7.298 14.979,6.775 14.889,6.273H7.668V9.239H11.794C11.616,10.197 11.076,11.009 10.264,11.553V13.477H12.742C14.192,12.142 15.028,10.177 15.028,7.842Z"
android:fillColor="#4285F4"
android:fillType="evenOdd"/>
<path
android:pathData="M7.668,15.333C9.738,15.333 11.473,14.646 12.742,13.476L10.264,11.552C9.577,12.012 8.699,12.284 7.668,12.284C5.671,12.284 3.981,10.935 3.378,9.123H0.816V11.109C2.078,13.615 4.671,15.333 7.668,15.333Z"
android:fillColor="#34A853"
android:fillType="evenOdd"/>
<path
android:pathData="M3.377,9.124C3.223,8.664 3.136,8.173 3.136,7.668C3.136,7.162 3.223,6.671 3.377,6.211V4.225H0.815C0.296,5.26 0,6.431 0,7.668C0,8.905 0.296,10.076 0.815,11.111L3.377,9.124Z"
android:fillColor="#FBBC05"
android:fillType="evenOdd"/>
<path
android:pathData="M7.668,3.049C8.793,3.049 9.804,3.436 10.598,4.196L12.797,1.997C11.47,0.76 9.734,0 7.668,0C4.671,0 2.078,1.718 0.816,4.224L3.378,6.21C3.981,4.398 5.671,3.049 7.668,3.049Z"
android:fillColor="#EA4335"
android:fillType="evenOdd"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fun AppNavHost() {
}
val start =
contributors
.firstOrNull { it.graphRoute == NavRoutes.MainGraph }
.firstOrNull { it.graphRoute == NavRoutes.LoginGraph }
?.graphRoute
?: error("해당 Graph를 찾을 수 없습니다.")
val duration = 300
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable

@Serializable
data class LoginRequest(
val code: String,
val idToken: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.POST

interface AuthService {
@POST("auth/google")
@POST("auth/google/token")
suspend fun googleLogin(
@Body request: LoginRequest,
): LoginResponse
Expand Down
1 change: 1 addition & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ android {

dependencies {
implementation(projects.core.datastore)
implementation(projects.core.token)
}
5 changes: 5 additions & 0 deletions data/src/main/java/com/twix/data/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.twix.data.di

import com.twix.data.repository.DefaultAuthRepository
import com.twix.data.repository.DefaultOnboardingRepository
import com.twix.domain.repository.AuthRepository
import com.twix.domain.repository.OnBoardingRepository
import org.koin.dsl.module

Expand All @@ -9,4 +11,7 @@ internal val repositoryModule =
single<OnBoardingRepository> {
DefaultOnboardingRepository(get())
}
single<AuthRepository> {
DefaultAuthRepository(get(), get())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.twix.data.repository

import com.twix.domain.login.LoginType
import com.twix.domain.repository.AuthRepository
import com.twix.network.model.request.LoginRequest
import com.twix.network.service.AuthService
import com.twix.token.TokenProvider

class DefaultAuthRepository(
private val service: AuthService,
private val tokenProvider: TokenProvider,
) : AuthRepository {
override suspend fun login(
idToken: String,
type: LoginType,
) {
val response =
when (type) {
LoginType.GOOGLE -> service.googleLogin(LoginRequest(idToken))
LoginType.KAKAO -> return
}

tokenProvider.saveToken(response.accessToken, response.refreshToken)
}
}
7 changes: 7 additions & 0 deletions domain/src/main/java/com/twix/domain/login/LoginProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.twix.domain.login

interface LoginProvider {
suspend fun login(): LoginResult

suspend fun logout(): Result<Unit>
}
14 changes: 14 additions & 0 deletions domain/src/main/java/com/twix/domain/login/LoginResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.twix.domain.login

sealed interface LoginResult {
data class Success(
val idToken: String,
val type: LoginType,
) : LoginResult

data object Cancel : LoginResult

data class Failure(
val throwable: Throwable?,
) : LoginResult
}
6 changes: 6 additions & 0 deletions domain/src/main/java/com/twix/domain/login/LoginType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.twix.domain.login

enum class LoginType {
GOOGLE,
KAKAO,
}
10 changes: 10 additions & 0 deletions domain/src/main/java/com/twix/domain/repository/AuthRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.twix.domain.repository

import com.twix.domain.login.LoginType

interface AuthRepository {
suspend fun login(
idToken: String,
type: LoginType,
)
}
28 changes: 28 additions & 0 deletions feature/login/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
import java.util.Properties
import kotlin.apply

plugins {
alias(libs.plugins.twix.feature)
}

val localProperties =
Properties().apply {
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
load(localPropertiesFile.inputStream())
}
}

android {
namespace = "com.twix.login"

buildFeatures {
buildConfig = true
}

defaultConfig {
buildConfigField(
"String",
"GOOGLE_CLIENT_ID",
"\"${localProperties.getProperty("google_client_id")}\"",
)
}
}
dependencies {
implementation(libs.googleid)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.play.services.auth)
}
16 changes: 16 additions & 0 deletions feature/login/src/main/java/com/twix/login/LoginProviderFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.twix.login

import com.twix.domain.login.LoginProvider
import com.twix.domain.login.LoginType

class LoginProviderFactory(
private val providers: Map<LoginType, LoginProvider>,
) {
operator fun get(type: LoginType): LoginProvider =
providers[type]
?: throw IllegalArgumentException(UNSUPPORTED_LOGIN_TYPE.format(type))

companion object {
private const val UNSUPPORTED_LOGIN_TYPE = "Unsupported login type: %s"
}
}
71 changes: 66 additions & 5 deletions feature/login/src/main/java/com/twix/login/LoginScreen.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,84 @@
package com.twix.login

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.twix.designsystem.components.toast.ToastManager
import com.twix.designsystem.components.toast.model.ToastData
import com.twix.designsystem.components.toast.model.ToastType
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.login.LoginType
import com.twix.login.LoginProviderFactory
import com.twix.login.component.LoginButton
import com.twix.login.model.LoginIntent
import com.twix.login.model.LoginSideEffect
import com.twix.ui.base.ObserveAsEvents
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject

@Composable
fun LoginScreen() {
LoginContent()
fun LoginRoute(
navigateToHome: () -> Unit,
navigateToOnBoarding: () -> Unit,
toastManager: ToastManager = koinInject(),
loginProvider: LoginProviderFactory = koinInject(),
viewModel: LoginViewModel = koinViewModel(),
) {
val coroutineScope = rememberCoroutineScope()

val loginFailMessage = stringResource(R.string.login_fail_message)
ObserveAsEvents(viewModel.sideEffect) { sideEffect ->
when (sideEffect) {
LoginSideEffect.NavigateToHome -> navigateToHome()
LoginSideEffect.NavigateToOnBoarding -> navigateToOnBoarding()
LoginSideEffect.ShowLoginFailToast -> {
toastManager.tryShow(ToastData(loginFailMessage, ToastType.ERROR))
}
}
}

LoginScreen { type ->
coroutineScope.launch {
viewModel.dispatch(LoginIntent.Login(loginProvider[type].login()))
}
}
}

@Composable
private fun LoginContent() {
private fun LoginScreen(onClickLogin: (LoginType) -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text("임시 화면입니다.")
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
// LoginType.entries.forEach { type ->
LoginButton(type = LoginType.GOOGLE, onClickLogin = onClickLogin)
// }
}
}
}

@Preview(showBackground = true)
@Composable
private fun LoginScreenPreview() {
TwixTheme {
LoginScreen(onClickLogin = {})
}
}
35 changes: 35 additions & 0 deletions feature/login/src/main/java/com/twix/login/LoginViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.twix.login

import androidx.lifecycle.viewModelScope
import com.twix.domain.login.LoginResult
import com.twix.domain.repository.AuthRepository
import com.twix.login.model.LoginIntent
import com.twix.login.model.LoginSideEffect
import com.twix.login.model.LoginUiState
import com.twix.ui.base.BaseViewModel
import kotlinx.coroutines.launch

class LoginViewModel(
private val authRepository: AuthRepository,
) : BaseViewModel<LoginUiState, LoginIntent, LoginSideEffect>(LoginUiState()) {
override suspend fun handleIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.Login -> login(intent.result)
}
}

private fun login(result: LoginResult) {
viewModelScope.launch {
when (result) {
is LoginResult.Success -> {
authRepository.login(result.idToken, result.type)
emitSideEffect(LoginSideEffect.NavigateToHome)
}
is LoginResult.Failure -> {
emitSideEffect(LoginSideEffect.ShowLoginFailToast)
}
LoginResult.Cancel -> Unit
}
}
}
}
Loading