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
8 changes: 0 additions & 8 deletions .github/workflows/develop_PR_builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,3 @@ jobs:
BASE_URL: ${{ secrets.BASE_URL }}
run: |
echo base.url=\"$BASE_URL\" >> local.properties
- name: Notify Slack on Pull Request
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took,pullRequest
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: always()
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ android {
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
buildConfigField("String", "KAKAO_API_KEY", properties["KAKAO_API_KEY"].toString())
buildConfigField("String", "GOOGLE_CLIENT_ID", properties["GOOGLE_CLIENT_ID"].toString())
buildConfigField("String", "GOOGLE_CLIENT_SECRET", properties["GOOGLE_CLIENT_SECRET"].toString())
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = properties["KAKAO_NATIVE_APP_KEY"].toString()
}

Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/sopt/noostak/di/GoogleIdModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ object GoogleIdModule {
fun provideGoogleClientId(): String {
return BuildConfig.GOOGLE_CLIENT_ID
}

@Provides
@Singleton
@Named("GoogleClientSecret")
fun provideGoogleClientSecret(): String {
return BuildConfig.GOOGLE_CLIENT_SECRET
}
}
20 changes: 10 additions & 10 deletions app/src/main/java/com/sopt/noostak/di/TokenInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,18 @@ class TokenInterceptor @Inject constructor(
val refreshToken = preferenceDatasource.refreshToken.first()

return try {
val tokenResult = runBlocking(Dispatchers.IO) {
val tokenResult = withContext(Dispatchers.IO) {
authService.postReissueToken(refreshToken)
}
when (tokenResult.status == SUCCESS) {
true -> {
preferenceDatasource.updateAccessToken(
BEARER + tokenResult.result?.accessToken
)
true
}

false -> false
if (tokenResult.status == SUCCESS && tokenResult.result != null) {
tokenResult.result?.let { result ->
preferenceDatasource.updateAccessToken(BEARER + result.accessToken)
preferenceDatasource.updateRefreshToken(BEARER + result.refreshToken)
}
true
} else {
false
}
} catch (e: Exception) {
false
Expand Down Expand Up @@ -103,7 +103,7 @@ class TokenInterceptor @Inject constructor(
).build()

companion object {
const val SUCCESS = 200
const val SUCCESS = 201
const val CODE_TOKEN_EXPIRE = 401
const val AUTHORIZATION = "Authorization"
const val BEARER = "Bearer "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable
data class ResponsePostReissueTokenDto(
@SerialName("accessToken") val accessToken: String,
@SerialName("refreshToken") val refreshToken: String,
@SerialName("authType") val authType: String
@SerialName("authId") val authId: String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 이게 달라서 재발급이 안됐던걸까유?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요것도 그렇고, SUCCESS 코드가 200으로 되어있어서 재발급이 안된거였으요..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇구나 딩딩딩딩딩

)
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ kakao = "2.20.1"
# Google
google-id = "1.1.1"
play-services-auth = "21.1.0"

viewpager-indicator = "5.0"
credentials = "1.2.0-alpha02"

# Hilt
hilt = "2.52"
Expand Down Expand Up @@ -164,6 +164,8 @@ kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao"
# Google
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" }
google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "google-id" }
credentials-core = { group = "androidx.credentials", name = "credentials", version.ref = "credentials" }
credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentials" }

# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } # Hilt Android 라이브러리
Expand Down
2 changes: 2 additions & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,6 @@ dependencies {
// Google
implementation(libs.play.services.auth)
implementation(libs.google.id)
implementation(libs.credentials.core)
implementation(libs.credentials.play.services.auth)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.sopt.presentation.auth.login

import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -24,13 +28,19 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.common.api.ApiException
import com.sopt.core.designsystem.component.dialog.NoostakDialog
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
import com.sopt.core.designsystem.theme.NoostakTheme
import com.sopt.core.extension.toast
import com.sopt.core.type.DialogType
import com.sopt.presentation.R
import com.sopt.presentation.auth.component.LoginButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@Composable
fun LoginRoute(
Expand All @@ -40,6 +50,38 @@ fun LoginRoute(
) {
val context = LocalContext.current
val showDialog by loginViewModel.showDialog.collectAsStateWithLifecycle()
val googleSignInIntent by loginViewModel.googleSignInIntent.collectAsStateWithLifecycle()

val googleSignInLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) {
loginViewModel.showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
return@rememberLauncherForActivityResult
}

val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
CoroutineScope(Dispatchers.Main).launch {
try {
val account: GoogleSignInAccount = task.getResult(ApiException::class.java)
val authCode = account.serverAuthCode
if (!authCode.isNullOrBlank()) {
loginViewModel.exchangeAuthCodeForAccessToken(authCode)
} else {
loginViewModel.showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
}
} catch (e: Exception) {
loginViewModel.showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
}
}
loginViewModel.clearGoogleSignInIntent()
}

LaunchedEffect(googleSignInIntent) {
googleSignInIntent?.let { intent ->
googleSignInLauncher.launch(intent)
}
}

LaunchedEffect(loginViewModel.sideEffects) {
loginViewModel.sideEffects.collect { sideEffect ->
Expand All @@ -63,7 +105,10 @@ fun LoginRoute(
loginViewModel.showDialog(dialogType, false)
when (dialogType) {
DialogType.NETWORK_LOGIN_KAKAO_FAILURE -> loginViewModel.kakaoLogin(context)
DialogType.NETWORK_LOGIN_GOOGLE_FAILURE -> loginViewModel.googleLogin(context)
DialogType.NETWORK_LOGIN_GOOGLE_FAILURE -> loginViewModel.prepareGoogleSignInIntent(
context
)

else -> Unit
}
},
Expand All @@ -74,7 +119,7 @@ fun LoginRoute(

LoginScreen(
onKakaoLoginClick = { loginViewModel.kakaoLogin(context) },
onGoogleLoginClick = { loginViewModel.googleLogin(context) }
onGoogleLoginClick = { loginViewModel.prepareGoogleSignInIntent(context) }
)
}

Expand All @@ -88,6 +133,7 @@ fun LoginScreen(
Column(
modifier = Modifier
.fillMaxSize()
.background(NoostakTheme.colors.blue600)
.statusBarsPadding()
.navigationBarsPadding()
.padding(dimensionResource(R.dimen.horizontal_padding)),
Expand Down Expand Up @@ -122,8 +168,8 @@ private fun SocialLoginBottom(
) {
Text(
text = stringResource(R.string.tv_login_description),
color = NoostakTheme.colors.gray900,
style = NoostakTheme.typography.c3Regular,
color = NoostakTheme.colors.white,
style = NoostakTheme.typography.c3SemiBold,
modifier = Modifier.padding(bottom = 8.dp)
)
LoginButton(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.sopt.presentation.auth.login

import android.content.Context
import android.content.Intent
import androidx.annotation.StringRes
import androidx.credentials.Credential
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.lifecycle.viewModelScope
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.kakao.sdk.auth.model.OAuthToken
import com.kakao.sdk.common.model.ClientError
import com.kakao.sdk.common.model.ClientErrorCause
Expand All @@ -19,28 +18,50 @@ import com.sopt.domain.repository.UserInfoRepository
import com.sopt.domain.usecase.PostSocialLoginUseCase
import com.sopt.presentation.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import javax.inject.Inject
import javax.inject.Named

@HiltViewModel
class LoginViewModel @Inject constructor(
@Named("GoogleClientId") private val googleClientId: String,
@Named("GoogleClientSecret") private val googleClientSecret: String,
private val userInfoRepository: UserInfoRepository,
private val postSocialLoginUseCase: PostSocialLoginUseCase
) : BaseViewModel<LoginSideEffect>() {

private val _showDialog = MutableStateFlow(Pair(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, false))
val showDialog: StateFlow<Pair<DialogType, Boolean>> get() = _showDialog

private val _googleSignInIntent = MutableStateFlow<Intent?>(null)
val googleSignInIntent: StateFlow<Intent?> get() = _googleSignInIntent

fun showDialog(dialogType: DialogType, isVisible: Boolean) {
_showDialog.update { it.copy(first = dialogType, second = isVisible) }
}

fun prepareGoogleSignInIntent(context: Context) {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestServerAuthCode(googleClientId, true)
.build()
val client: GoogleSignInClient = GoogleSignIn.getClient(context, gso)
_googleSignInIntent.value = client.signInIntent
}

fun clearGoogleSignInIntent() {
_googleSignInIntent.value = null
}

// Kakao Login
fun kakaoLogin(context: Context) {
val loginCallback: (OAuthToken?, Throwable?) -> Unit = this::handleKakaoLoginResult
Expand Down Expand Up @@ -69,39 +90,45 @@ class LoginViewModel @Inject constructor(
}
}

// Google Login
fun googleLogin(context: Context) {
val credentialManager = CredentialManager.create(context)

val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(googleClientId)
.setAutoSelectEnabled(true)
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

viewModelScope.launch {
runCatching {
val result = credentialManager.getCredential(context, request)
handleGoogleLoginResult(result.credential)
}.onFailure { exception ->
handleError(exception, R.string.toast_google_login_failed)
// Get Google AccessToken
fun exchangeAuthCodeForAccessToken(authCode: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val requestBody = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("code", authCode)
.add("client_id", googleClientId)
.add("client_secret", googleClientSecret)
.add("redirect_uri", "")
.build()

val request = Request.Builder()
.url("https://oauth2.googleapis.com/token")
.post(requestBody)
.build()

val client = OkHttpClient()
val response = client.newCall(request).execute()
val jsonString = response.body?.string()
val json = JSONObject(jsonString ?: "")

val accessToken = json.optString("access_token")

if (accessToken.isNotBlank()) {
onGoogleAccessTokenReceived(accessToken)
} else {
showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
}
} catch (e: Exception) {
showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
}
}
}

private fun handleGoogleLoginResult(credential: Credential) {
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
postSocialLogin(googleIdTokenCredential.id, GOOGLE)
showToast(R.string.toast_google_login_success)
} else {
showDialog(DialogType.NETWORK_LOGIN_GOOGLE_FAILURE, true)
}
// Google Login
private fun onGoogleAccessTokenReceived(accessToken: String) {
postSocialLogin(BEARER + accessToken, GOOGLE)
showToast(R.string.toast_google_login_success)
}

private fun handleError(error: Throwable?, @StringRes errorMessageResId: Int) {
Expand Down Expand Up @@ -134,7 +161,6 @@ class LoginViewModel @Inject constructor(
},
onFailure = { error ->
emitSideEffect(LoginSideEffect.NavigateToOnboarding(accessToken, socialType))
Timber.e("postSocialLogin Failed: ${error.message}")
}
)
}
Expand Down
Loading