본문 바로가기

Android/note

Firebase Auth + 구글 / 페이스북 로그인 (with Compose)

간간히 다운로드수가 발생하지만 정작 가입하는 사람이 없다!

 

오마이갓!

 

이메일로 가입해야하고 -> 이메일로 로그인해야하니 귀찮아서 더이상 진행을 안하는 것 같다는 생각이 들었다.

그래서 파이어베이스와 연동해서 사용할 수 있는 구글/페이스북 로그인을 구현해보았다.

사실 카카오/네이버 로그인도 넣어보고싶은데 서버가 있어야 진행이 가능할 것 같아서 일단은 보류하기로 했다!

 


PlayStore

 

Senty - 선물 기록 어플리케이션 - Google Play 앱

소중한 사람들과 정성스레 주고받은 선물을 기록해보세요.

play.google.com

 


결과화면

 

원형 아이콘으로 깔끔하게 버튼을 만들어주었다.

 

구글 버튼은 아래의 링크에서 다운로드 받아 사용했고,

 

로그인 브랜드 가이드라인  |  Google ID 플랫폼  |  Google for Developers

로그인 브랜드 가이드라인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 모바일 또는 웹 앱에서 기본적인 profile 또는 email 범위로 Google 로그인을 사용하고

developers.google.com

 

페이스북은 따로 제공하고 있지 않는건지 찾지못하겠어서 .. svg 파일을 검색해서 최대한 지금의 로고와 같은 걸로 다운로드 받아서 적용해주었다. 근데 그 곳이 이제 위키피디아 ..

 

파일:Facebook f logo (2019).svg - 위키백과, 우리 모두의 백과사전

로고가 변경되면이 파일을 덮어 쓰지 말고 다른 이름' 새 로고를 업로드하고 역사를 위해 여기에 보관하십시오!

ko.m.wikipedia.org

 


 

자 이제 시작이야~ 구현을~

1️⃣ 제공 업체 추가

가장 먼저 파이어베이스의 인증부분에서 제공업체를 추가해주어야 한다.

 

2️⃣ 페이스북 로그인 구현

 

사실 파이어베이스랑 메타 개발자 문서?에 나와있는 대로 쭉쭉 따라해주면 된다

 

파이어베이스 인증 - 페이스북 로그인 문서

 

Android에서 Facebook 로그인을 사용하여 인증하기  |  Firebase Authentication

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 Android에서 Facebook 로그인을 사용하여 인증하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저

firebase.google.com

 

 

페이스북 개발자 문서

 

시작하기 - Android용 Facebook SDK - 문서 - Meta for Developers

Android용 Facebook SDK 시작하기 이 문서에서는 Android용 Facebook SDK를 사용하여 Facebook과 Android 앱의 통합을 시작하는 방법을 설명합니다. Android용 Facebook SDK의 현재 버전은 12.0.0이고 Android API 15가 필요

developers.facebook.com

 

사전 준비가 완료되었다면, 코드를 봐보시죠

 

// AndroidManifest.xml

<application>
    ...
    
    <meta-data
        tools:replace="android:value"
        android:name="com.facebook.sdk.ApplicationId"
        android:value="${FACEBOOK_APP_ID}" />
	<meta-data
        tools:replace="android:value"
        android:name="com.facebook.sdk.ClientToken"
        android:value="${FACEBOOK_CLIENT_TOKEN}" />
    
    ...
</application>

 

tools:reaplce="android:value" 를 작성하지 않았더니 오류가 나길래 검색을 통해서 키워드를 넣어주었다!

 

// 메타 개발자 문서에 나와있는 AndroidManifest.xml

<application android:label="&#064;string/app_name" ...>
    ...
    
    <meta-data 
        android:name="com.facebook.sdk.ApplicationId" 
        android:value="@string/facebook_app_id" />
    <meta-data
        android:name="com.facebook.sdk.ClientToken" 
        android:value="@string/facebook_client_token" />
        
    ...
</application>

 

이건 메타 개발자 문서에 나와있는 manifest이다.

아마 @string/으로 가져온 것이 아닌 BuildConfig를 통해 가져왔기 때문에 오류가 발생했고, 오류를 해결하기 위해 해당 키워드를 넣어주는 것 같았다.

 

// LoginScreen.kt

@Composable
private fun SocialLogins(
    modifier: Modifier = Modifier,
    sendSnackbarMessage: (String) -> Unit,
    onFacebookLoginClick: (String) -> Unit,
    onGoogleLoginClick: () -> Unit,
) {
    val facebookLoginManager = LoginManager.getInstance()
    val callbackManager = remember { CallbackManager.Factory.create() }
    val launcher = rememberLauncherForActivityResult(
        facebookLoginManager.createLogInActivityResultContract(callbackManager, null)) {
    }

    DisposableEffect(Unit) {
        facebookLoginManager.registerCallback(callbackManager, object : FacebookCallback<LoginResult> {
            override fun onCancel() {}

            override fun onError(error: FacebookException) {
                sendSnackbarMessage(error.stackTraceToString())
            }

            override fun onSuccess(result: LoginResult) {
                val token = result.accessToken.token
                onFacebookLoginClick(token)
            }
        })
        onDispose {
            facebookLoginManager.unregisterCallback(callbackManager)
        }
    }

    Column(modifier = modifier) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 32.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Divider(modifier = Modifier.weight(1f))
            Text(
                text = "다음 계정으로 로그인",
                style = MaterialTheme.typography.labelLarge,
                modifier = Modifier.weight(2f),
                textAlign = TextAlign.Center
            )
            Divider(modifier = Modifier.weight(1f))
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 32.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            Image(
                painter = painterResource(
                    id = R.drawable.login_symbol_facebook
                ),
                contentDescription = "Facebook Login Button",
                modifier = Modifier
                    .size(40.dp)
                    .clip(CircleShape)
                    .clickable { launcher.launch(listOf("email", "public_profile")) }
            )

            ...
        }
    }
}

 

소셜 로그인을 위한 컴포저블 함수를 따로 만들어놓았다.

LoginScreen에서 모두 처리가 가능하지만, 나는 onFacebookLoginClick() 함수를 만들어 viewModel로 토큰을 넘겨주었다.

 

// LoginScreen.kt

@Composable
fun LoginScreen(
    vm: LoginViewModel = hiltViewModel(),
    onSuccessLogin: () -> Unit,
    onClickSignUp: () -> Unit,
) {
    ...
    
    LoginContents(
        ...
        onFacebookLoginClick = { vm.signInWithFacebook(it) },
        ...
    )
}

 

// LoginViewModel.kt

fun signInWithFacebook(facebookToken: String) {
    loading.value = true

    val credential = FacebookAuthProvider.getCredential(facebookToken)
    signInWithCredential(credential)
}

 

LoginScreen 에서 넘어온 토큰을 통해 Credential을 만들고, 해당 자격 증명을 통해 파이어베이스 인증에 등록을 시켜준다!

 

signInWithCredential() 부분은 구글 로그인에서도 중복적으로 사용이 되어서 따로 메소드로 빼내어주었다.

 

// LoginViewModel.kt

private fun signInWithCredential(credential: AuthCredential) {
    viewModelScope.launch {
        try {
            val authResult = firebaseAuth.signInWithCredential(credential).await()
            val isNewUser = authResult.additionalUserInfo?.isNewUser ?: false

            if (isNewUser) {
                firebaseAuth.currentUser?.apply {
                    val user = UserEntity(
                        uid = uid,
                    )

                    coroutineScope {
                        val result = async { accountRepository.insertUser(uid, user) }.await()

                        if (result.isSuccessful) {
                            loading.value = false
                            _result.value = true
                        } else {
                            sendSnackbarMessage("로그인에 실패하였습니다.")
                        }
                    }
                }
            } else {
                loading.value = false
                _result.value = true
            }
        } catch (firebaseAuthException: FirebaseAuthInvalidUserException) {
            sendSnackbarMessage("계정이 비활성화 되어있습니다.")
        } catch (firebaseAuthInvalidCredentialsException: FirebaseAuthInvalidCredentialsException) {
            // 자격 증명이 만료된 경우
        }
    }
}

 

생성된 Credential을 통해 파이어베이스 인증에 로그인을 하고, UID를 가지고 있지 않다면(새로 등록된 유저라면) 유저를 생성한다.

 

여기서 UserEntity로 uid 하나만 받고 있는 것을 볼 수 있다.

uid를 하나만 받은 이유는 아직까지는 유저의 다른 정보를 받을 필요가 없다고 판단이 되었기 때문이다.

모두 사용자가 생성하는 데이터로 앱을 이용할 수 있다. 사용자가 가지고 있는 데이터를 사용하는 일이 없음

가령 사용자의 이메일을 다른 사람들에게 보여준다거나, 프로필 사진을 보여준다거나 이런일이 없으니 정보를 저장할 필요가 없다고 생각되었다.

추후에 닉네임을 도입할 생각이지만 도입하게 되면 사용자에게 닉네임을 입력받을 것이라 UserEntity로 uid와 생성된 날짜만 일단 저장해주었다.

 

사실 이렇게 하면 끝 .. 연동 끝.. 넘 쉽죵...

 

사실 구글도 비슷하다!

 


3️⃣ 구글 로그인 구현

바로 코드로 모시겠습니다.

3️⃣ - 1️⃣ 구글 버튼 등록

// LoginScreen.kt

@Composable
private fun SocialLogins(
    modifier: Modifier = Modifier,
    sendSnackbarMessage: (String) -> Unit,
    onFacebookLoginClick: (String) -> Unit,
    onGoogleLoginClick: () -> Unit,
) {
    ...

    Column(modifier = modifier) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 32.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Divider(modifier = Modifier.weight(1f))
            Text(
                text = "다음 계정으로 로그인",
                style = MaterialTheme.typography.labelLarge,
                modifier = Modifier.weight(2f),
                textAlign = TextAlign.Center
            )
            Divider(modifier = Modifier.weight(1f))
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 32.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            ...


            // 구글 로그인 버튼 등록
            Image(
                painter = painterResource(
                    id = R.drawable.login_symbol_google
                ),
                contentDescription = "Google Login Button",
                modifier = Modifier
                    .clip(CircleShape)
                    .clickable { onGoogleLoginClick() }
            )
        }
    }
}

 

3️⃣ - 2️⃣ 구글 로그인 콜백

// LoginScreen.kt

@Composable
fun LoginScreen(
    vm: LoginViewModel = hiltViewModel(),
    onSuccessLogin: () -> Unit,
    onClickSignUp: () -> Unit,
) {
    val loginResult by vm.result.collectAsStateWithLifecycle()
    val snackBarHostState = remember { SnackbarHostState() }

    val googleAuthLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartIntentSenderForResult()
    ) { result ->
            when (result.resultCode) {
                Activity.RESULT_OK -> {
                    val credentials = vm.signInClient.getSignInCredentialFromIntent(result.data)
                    val googleIdToken = credentials.googleIdToken

                    googleIdToken?.let { token ->
                        vm.signInWithGoogle(token)
                    }
                }
            }
        }
        
    ...
    
    LoginContents(
        ...
        
        onGoogleLoginClick = {
            vm.signInClient.signOut()
            vm.signInClient.beginSignIn(vm.signInRequest)
                .addOnSuccessListener { result ->
                    val intentSenderRequest = IntentSenderRequest
                        .Builder(result.pendingIntent.intentSender)
                        .build()
                    googleAuthLauncher.launch(intentSenderRequest)
                }
        }
    )
}

 

로그인 버튼을 클릭하면 구글 화면으로 이동할 intent를 시작시켜주고, intent의 결과를 처리해주면 끝 ..

 

// LoginViewModel.kt

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val firebaseAuth: FirebaseAuth,
    private val accountRepository: AccountRepository,
    val signInClient: SignInClient,
    val signInRequest: BeginSignInRequest
) : ViewModel() {

    ...
    
    fun signInWithGoogle(googleIdToken: String) {
        loading.value = true

        val googleCredential = GoogleAuthProvider.getCredential(googleIdToken, null)
        signInWithCredential(googleCredential)
    }
}

 

페이스북이랑 비슷하지 않나용?

여기서도 토큰을 LoginScreen에서 넘겨주면 그걸로 Credential을 생성하여 signInWithCredential()로 넘겨주면 된다!

 

아차차 LoginViewModel에서 구글 로그인을 구현하려면 SignInClient와 BeginSignInRequest 객체를 생성해주어야 하는데, 나는 Hilt를 사용하고 있어서 DI로 주입시켜주었다.

 

// FirebaseModule.kt

@Provides
fun provideSingInClient(
    @ApplicationContext context: Context,
): SignInClient {
    return Identity.getSignInClient(context)
}

@Provides
fun provideSignInRequest(): BeginSignInRequest {
    return BeginSignInRequest.builder()
        .setGoogleIdTokenRequestOptions(
            BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                .setSupported(true)
                .setServerClientId(BuildConfig.GOOGLE_CLOUD_WEB_CLIENT_ID)
                .setFilterByAuthorizedAccounts(true)
                .build()
            )
        .setAutoSelectEnabled(true)
        .build()
}

 

구글/페이스북으로 로그인을 하면 아래와 같이 파이어베이스 인증 화면에 나타난다!

구글은 왜 식별자가 - 인지는 모르겠다 ..!

 

 

그럼 끝...입니다~

 

짜잔..~

 

 

참고

https://www.composables.com/tutorials/firebase-auth-facebook