Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1011] Google Sign In 설정 #1014

Open
wants to merge 12 commits into
base: feature/#1011-login-mock-server
Choose a base branch
from
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.fragment.ktx)
implementation(libs.androidx.credential)
implementation(libs.androidx.credential.play.service)

androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.bundles.compose.test)
Expand All @@ -179,6 +181,7 @@ dependencies {
implementation(libs.coil.core)
implementation(libs.profileinstaller)
implementation(libs.firebase.messaging.lifecycle.ktx)
implementation(libs.google.id.identity)
}

secrets {
Expand Down
7 changes: 7 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,10 @@
-dontwarn com.google.auto.service.AutoService
-dontwarn net.ltgt.gradle.incap.IncrementalAnnotationProcessor
-dontwarn net.ltgt.gradle.incap.IncrementalAnnotationProcessorType

##--------------- Begin: androidx.credential ----------
-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
*;
}
##--------------- End: androidx.credential ------------
120 changes: 82 additions & 38 deletions app/src/main/java/org/sopt/official/feature/auth/AuthActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,31 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.sopt.official.BuildConfig
import org.sopt.official.R
import org.sopt.official.auth.PlaygroundAuth
import org.sopt.official.auth.data.PlaygroundAuthDatasource
import org.sopt.official.auth.impl.api.AuthService
import org.sopt.official.auth.impl.model.request.AuthRequest
import org.sopt.official.auth.model.UserStatus
import org.sopt.official.common.coroutines.suspendRunCatching
import org.sopt.official.common.di.Auth
import org.sopt.official.common.view.toast
import org.sopt.official.designsystem.SoptTheme
import org.sopt.official.feature.home.HomeActivity
import org.sopt.official.feature.mypage.web.WebUrlConstant
import org.sopt.official.network.model.response.OAuthToken
import org.sopt.official.network.persistence.SoptDataStore
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
Expand All @@ -74,11 +78,15 @@ class AuthActivity : AppCompatActivity() {
SoptTheme {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()

LaunchedEffect(true) {
if (dataStore.accessToken.isNotEmpty()) {
startActivity(
HomeActivity.getIntent(context, HomeActivity.StartArgs(UserStatus.of(dataStore.userStatus)))
HomeActivity.getIntent(
context,
HomeActivity.StartArgs(UserStatus.of(dataStore.userStatus))
)
)
}
}
Expand All @@ -89,27 +97,38 @@ class AuthActivity : AppCompatActivity() {
getString(R.string.toolbar_notification),
NotificationManager.IMPORTANCE_HIGH
).apply {
setSound(null, null)
setSound(
null,
null
)
enableLights(false)
enableVibration(false)
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
getSystemService<NotificationManager>()?.createNotificationChannel(this)
}
}

LaunchedEffect(viewModel.uiEvent, lifecycleOwner) {
viewModel.uiEvent.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle)
.collect { event ->
when (event) {
is AuthUiEvent.Success -> startActivity(
HomeActivity.getIntent(context, HomeActivity.StartArgs(event.userStatus))
LaunchedEffect(
viewModel.uiEvent,
lifecycleOwner
) {
viewModel.uiEvent.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle).collect { event ->
when (event) {
is AuthUiEvent.Success -> startActivity(
HomeActivity.getIntent(
context,
HomeActivity.StartArgs(event.userStatus)
)
)

is AuthUiEvent.Failure -> startActivity(
HomeActivity.getIntent(context, HomeActivity.StartArgs(UserStatus.UNAUTHENTICATED))
is AuthUiEvent.Failure -> startActivity(
HomeActivity.getIntent(
context,
HomeActivity.StartArgs(UserStatus.UNAUTHENTICATED)
)
}
)
}
}
}

AuthScreen(
Expand All @@ -124,40 +143,65 @@ class AuthActivity : AppCompatActivity() {
)
},
onGoogleLoginCLick = {
PlaygroundAuth.authorizeWithWebTab(
context = context,
isDebug = BuildConfig.DEBUG,
authDataSource = object : PlaygroundAuthDatasource {
override suspend fun oauth(code: String): Result<OAuthToken> {
return kotlin.runCatching {
authService
.authenticate(AuthRequest(code, dataStore.pushToken))
.toOAuthToken()
}
}
}
) {
it.onSuccess { token ->
lifecycleScope.launch {
viewModel.onLogin(token.toEntity())
val credentialManager = CredentialManager.create(context)
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(BuildConfig.SERVER_CLIENT_ID)
.setAutoSelectEnabled(true)
.build()
val credentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

scope.launch {
suspendRunCatching {
credentialManager.getCredential(
request = credentialRequest,
context = context
)
}.onSuccess {
if (it.credential is CustomCredential &&
it.credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
) {
val googleIdTokenCredential =
GoogleIdTokenCredential.createFrom(it.credential.data)
val idToken = googleIdTokenCredential.idToken
// TODO 로그인 성공시에 서버로 idToken을 전송해주세요.
context.toast("로그인 성공")
}
}.onFailure {
lifecycleScope.launch {
viewModel.onFailure(it)
}
Timber.e(it)
context.toast("로그인 실패")
}
}
},
onContactChannelClick = { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(WebUrlConstant.OPINION_KAKAO_CHAT))) },
onGoogleFormClick = { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(WebUrlConstant.SOPT_GOOGLE_FROM))) }
onContactChannelClick = {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(WebUrlConstant.OPINION_KAKAO_CHAT)
)
)
},
onGoogleFormClick = {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(WebUrlConstant.SOPT_GOOGLE_FROM)
)
)
}
)
}
}
}

companion object {
@JvmStatic
fun newInstance(context: Context) = Intent(context, AuthActivity::class.java).apply {
fun newInstance(context: Context) = Intent(
context,
AuthActivity::class.java
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal fun Project.configureAndroidCommonPlugin() {
val userStatusKeyAlias = properties["userStatusKeyAlias"] as? String ?: ""
val pushTokenKeyAlias = properties["pushTokenKeyAlias"] as? String ?: ""
val mockAuthApi = properties["mockAuthApi"] as? String ?: ""
val serverClientId = properties["serverClientId"] as? String ?: ""
buildConfigField("String", "SOPTAMP_API_KEY", apiKey)
buildConfigField("String", "SOPTAMP_DATA_STORE_KEY", dataStoreKey)
buildConfigField("String", "POKE_DATA_STORE_KEY", pokeDataStoreKey)
Expand All @@ -53,6 +54,7 @@ internal fun Project.configureAndroidCommonPlugin() {
buildConfigField("String", "USER_STATUS_KEY_ALIAS", userStatusKeyAlias)
buildConfigField("String", "PUSH_TOKEN_KEY_ALIAS", pushTokenKeyAlias)
buildConfigField("String", "MOCK_AUTH_API", mockAuthApi)
buildConfigField("String", "SERVER_CLIENT_ID", serverClientId)
}
buildFeatures.apply {
viewBinding = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.official.common.coroutines

import kotlinx.coroutines.TimeoutCancellationException
import kotlin.coroutines.cancellation.CancellationException

suspend inline fun <R> suspendRunCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (t: TimeoutCancellationException) {
Result.failure(t)
} catch (c: CancellationException) {
throw c
} catch (e: Throwable) {
Result.failure(e)
}
}
Comment on lines +6 to +16
Copy link
Member

Choose a reason for hiding this comment

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

오호 이 함수를 추가한 이유는 더 상세한 에러를 잡기 위함인가요...?

Copy link
Member Author

Choose a reason for hiding this comment

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

정확히는 저 CancellationException은 이 코루틴에서 에러가 발생했을 때 다른 코루틴으로의 예외전파를 시키는 것을 막지 않게 하기 위해서 추가한 코드에요

Reference - Kotlin/kotlinx.coroutines#1814

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import org.sopt.official.auth.data.RemotePlaygroundAuthDatasource
import org.sopt.official.auth.utils.PlaygroundUriProvider
import org.sopt.official.network.model.response.OAuthToken

@Deprecated("네이티브 구글 로그인으로 대체됩니다.")
object PlaygroundAuth {
fun authorizeWithWebTab(
context: Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ import androidx.lifecycle.lifecycleScope
import coil.load
import coil.transform.CircleCropTransformation
import dagger.hilt.android.AndroidEntryPoint
import java.io.Serializable
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand Down Expand Up @@ -66,6 +64,8 @@ import org.sopt.official.feature.poke.user.PokeUserListItemViewType
import org.sopt.official.feature.poke.util.addOnAnimationEndListener
import org.sopt.official.feature.poke.util.setRelationStrokeColor
import org.sopt.official.feature.poke.util.showPokeToast
import java.io.Serializable
import javax.inject.Inject

@AndroidEntryPoint
class FriendListSummaryActivity : AppCompatActivity() {
Expand Down Expand Up @@ -110,7 +110,7 @@ class FriendListSummaryActivity : AppCompatActivity() {

private fun initAppBar() {
binding.includeAppBar.apply {
toolbar.setOnClickListener { onBackPressed() }
toolbar.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
textViewTitle.text = getString(R.string.my_friend_title)
}
}
Expand Down
33 changes: 19 additions & 14 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ customtab = "1.8.0"
exifinterface = "1.3.7"
compose-bom = "2024.12.01"
androidx-paging = "3.3.5"
androidx-credential = "1.5.0-beta01"
desugarJdk = "2.1.4"

material = "1.12.0"
in-app-update = "2.1.0"
secret-gradle-plugin = "2.0.1"
google-id = "1.1.1"

junit = "4.13.2"
androidx-test-junit = "1.2.1"
Expand Down Expand Up @@ -93,10 +95,13 @@ startup = { module = "androidx.startup:startup-runtime", version.ref = "startup"
exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" }
desugarLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdk" }
customtab = { module = "androidx.browser:browser", version.ref = "customtab" }
androidx-credential = { module = "androidx.credentials:credentials", version.ref = "androidx-credential" }
androidx-credential-play-service = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "androidx-credential" }

material = { module = "com.google.android.material:material", version.ref = "material" }
inappupdate = { module = "com.google.android.play:app-update-ktx", version.ref = "in-app-update" }
secret-plugin = { group = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", name = "secrets-gradle-plugin", version.ref = "secret-gradle-plugin" }
google-id-identity = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "google-id" }

compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
compose-ui = { module = "androidx.compose.ui:ui" }
Expand Down Expand Up @@ -186,20 +191,20 @@ deeplink-dispatch-processor = { group = "com.airbnb", name = "deeplinkdispatch-p

[bundles]
compose = [
"compose-ui",
"compose-foundation",
"compose-ui-tooling",
"compose-activity",
"compose-animation",
"compose-viewmodel",
"compose-material",
"compose-material-three",
"compose-material-icons-extended",
"compose-material-icons",
"compose-runtime",
"compose-ui-tooling-preview",
"compose-hilt-navigation",
"compose-lottie",
"compose-ui",
"compose-foundation",
"compose-ui-tooling",
"compose-activity",
"compose-animation",
"compose-viewmodel",
"compose-material",
"compose-material-three",
"compose-material-icons-extended",
"compose-material-icons",
"compose-runtime",
"compose-ui-tooling-preview",
"compose-hilt-navigation",
"compose-lottie",
]
compose-test = ["compose-junit"]
compose-android-test = ["compose-ui-test"]
Expand Down