Skip to content

Commit

Permalink
[#30] 로그인 기능 추가
Browse files Browse the repository at this point in the history
- Firebase SiginIn 작업
  • Loading branch information
ethan-223 committed Sep 4, 2022
1 parent ad6d58b commit 8e6dece
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 4 deletions.
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,18 @@ dependencies {

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

// Lifecycle
def lifecycle_version = '2.4.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

// Firebase
implementation platform('com.google.firebase:firebase-bom:29.2.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.moyerun.moyeorun_android.common

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer

typealias EventLiveData<T> = LiveData<Event<T>>

class MutableEventLiveData<T> : MutableLiveData<Event<T>>() {

var event: T?
@Deprecated("getter is NOT supported!", level = DeprecationLevel.ERROR)
get() = throw UnsupportedOperationException()
set(value) {
if (value != null) {
setValue(Event(value))
}
}

fun postEvent(value: T?) {
if (value != null) {
postValue(Event(value))
}
}
}

/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {

var hasBeenHandled = false
private set // Allow external read but not write

/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}

/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import android.app.Activity
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.activity.ComponentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.moyerun.moyeorun_android.R
import com.moyerun.moyeorun_android.common.EventLiveData
import com.moyerun.moyeorun_android.common.EventObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

fun Activity.toast(msg: String, isShort: Boolean = true) {
Toast.makeText(this, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show()
Expand All @@ -19,4 +27,15 @@ inline fun FragmentActivity.showAllowingStateLoss(

fun Activity.showNetworkErrorToast() {
toast(getString(R.string.toast_network_error))
}

fun ComponentActivity.repeatOnStart(block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED, block) }
}

fun <T> ComponentActivity.observeEvent(
event: EventLiveData<T>,
onEventUnhandledContent: (T) -> Unit
) {
event.observe(this, EventObserver(onEventUnhandledContent))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.moyerun.moyeorun_android.R
import com.moyerun.moyeorun_android.common.EventLiveData
import com.moyerun.moyeorun_android.common.EventObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

fun Fragment.toast(msg: String, isShort: Boolean = false) {
Toast.makeText(context, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show()
Expand All @@ -24,4 +31,14 @@ inline fun FragmentManager?.showAllowingStateLoss(

fun Fragment.showNetworkErrorToast() {
toast(getString(R.string.toast_network_error))
}

fun Fragment.repeatOnStart(block: suspend CoroutineScope.() -> Unit) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block)
}
}

fun <T> Fragment.observeEvent(event: EventLiveData<T>, onEventUnhandledContent: (T) -> Unit) {
event.observe(viewLifecycleOwner, EventObserver(onEventUnhandledContent))
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Bundle
import android.provider.Settings
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.identity.BeginSignInRequest
import com.google.android.gms.auth.api.identity.Identity
Expand All @@ -15,13 +16,17 @@ import com.google.android.gms.common.api.CommonStatusCodes
import com.moyerun.moyeorun_android.BuildConfig
import com.moyerun.moyeorun_android.R
import com.moyerun.moyeorun_android.common.Lg
import com.moyerun.moyeorun_android.common.extension.observeEvent
import com.moyerun.moyeorun_android.common.extension.showNetworkErrorToast
import com.moyerun.moyeorun_android.common.extension.toast
import com.moyerun.moyeorun_android.databinding.ActivityLoginBinding
import dagger.hilt.android.AndroidEntryPoint


@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {

private val viewModel: LoginViewModel by viewModels()

private val oneTapClient: SignInClient by lazy { Identity.getSignInClient(this) }
private val signInRequest: BeginSignInRequest by lazy { getBeginSignInRequest() }

Expand All @@ -32,9 +37,8 @@ class LoginActivity : AppCompatActivity() {
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
//Todo: 서버에 보내서 인증 @winter223
// Todo: Firebase crashlytics userId 세팅
Lg.d("Success. token : $idToken")
Lg.i("Success. token : $idToken")
viewModel.signIn(idToken)
} else {
showUnknownErrorToast()
//Todo: #31 을 rebase 하고 주석 풀기
Expand Down Expand Up @@ -62,6 +66,22 @@ class LoginActivity : AppCompatActivity() {
val binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)

observeEvent(viewModel.loginEvent) {
when (it) {
LoginEvent.Success -> {
// Todo: 메인화면 진입
Lg.d("Login!")
}
LoginEvent.NewUser -> {
// Todo: 회원가입
Lg.d("New user!")
}
LoginEvent.Error -> {
showUnknownErrorToast()
}
}
}

binding.buttonLoginGoogle.setOnClickListener {
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener(this) { result ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moyerun.moyeorun_android.login

enum class LoginEvent {
Success, NewUser, Error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moyerun.moyeorun_android.login

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent

@Module
@InstallIn(ViewModelComponent::class)
class LoginModule {

@Provides
fun providesLoginRepository(): LoginRepository = LoginRepository()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.moyerun.moyeorun_android.login

import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.*
import java.lang.IllegalStateException
import kotlin.coroutines.resume

class LoginRepository(
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
) {

// Todo: 모여런 서버 붙일 때 반환 타입 조정
suspend fun signIn(idToken: String): ApiResponse<Unit> {
val firebaseSignInResult = withContext(coroutineDispatcher) { signInWithFirebaseCredential(idToken) }
if (firebaseSignInResult is ApiResponse.Failure) {
return firebaseSignInResult
}
val moyeorunSignInResult = withContext(coroutineDispatcher) { signInWithMoyeoRun(idToken) }
if (firebaseSignInResult is ApiResponse.Failure) {
Firebase.auth.signOut()
}
return withContext(coroutineDispatcher) { signInWithMoyeoRun(idToken) }
}

private suspend fun signInWithFirebaseCredential(idToken: String): ApiResponse<FirebaseUser> {
val firebaseAuth = Firebase.auth
return suspendCancellableCoroutine {
val firebaseCredential = GoogleAuthProvider.getCredential(idToken, null)
firebaseAuth.signInWithCredential(firebaseCredential)
.addOnCompleteListener { task ->
val firebaseUser = firebaseAuth.currentUser
if (firebaseUser != null) {
it.resume(ApiResponse.Success(firebaseUser))
} else {
it.resume(
ApiResponse.Failure(
task.exception
?: IllegalStateException("FirebaseAuth Failure: FirebaseUser and Task.exception is null")
)
)
}
}
}
}

private suspend fun signInWithMoyeoRun(idToken: String): ApiResponse<Unit> {
//Todo: 모여런 서버 signIn
return ApiResponse.Success(Unit)
}

// Todo: 임시. 네트워크 베이스 코드가 Rebase 되면 대체할 것
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Failure(val error: Throwable) : ApiResponse<Nothing>()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.moyerun.moyeorun_android.login

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.moyerun.moyeorun_android.common.EventLiveData
import com.moyerun.moyeorun_android.common.MutableEventLiveData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginRepository: LoginRepository
) : ViewModel() {

private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean>
get() = _isLoading

private val _loginEvent = MutableEventLiveData<LoginEvent>()
val loginEvent: EventLiveData<LoginEvent>
get() = _loginEvent

fun signIn(idToken: String) {
viewModelScope.launch {
_isLoading.value = true
val result = loginRepository.signIn(idToken)
if (result is LoginRepository.ApiResponse.Success) {
// Todo: Firebase crashlytics userId 세팅
_loginEvent.event = LoginEvent.Success
} else {
_loginEvent.event = LoginEvent.Error
}
_isLoading.value = false
}
}
}

0 comments on commit 8e6dece

Please sign in to comment.