관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 파이어베이스 전화번호 인증 구현 방법 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 파이어베이스 전화번호 인증 구현 방법

막무가내막내 2020. 11. 26. 20:27
728x90

 

[2021-05-16 업데이트]

 

오랜만에 안드로이드 관련 포스팅입니다.

복습도 할겸 이전에 했던 프로젝트를 보던 중 전화번호 인증 구현에 대해 포스팅을 해보려고합니다.

안드로이드와 코틀린을 요즘 못하고있네요 ㅠㅠ

 

 

 

 

공식문서를 참고했습니다.

firebase.google.com/docs/auth/android/phone-auth?hl=ko

 

Android에서 전화번호로 Firebase에 인증

Firebase 인증을 사용하면 사용자의 전화로 SMS 메시지를 전송하여 로그인하는 것이 가능합니다. 사용자는 SMS 메시지에 포함된 일회용 코드를 사용하여 로그인합니다. 앱에 전화번호 로그인을 추

firebase.google.com

추가로 제가 여기서 구현한 전화번호인증을 구현하려면 이미 파이어베이스 인증이 된 상태여야 합니다. 저 같은 경우 이 화면의 이전 로직에서 파이어베이스 이메일 로그인 인증을 한 상태이고 추가적으로 전화번호 인증을 한겁니다.

파이어베이스 auth 로그인 인증이 안된 경우 제가 구현한 전화번호 인증에서 전화번호를 onVerificationCompleted() 로 콜백으로 받을 수 없는 것 같습니다. 이를 위해서는 onCodeSent() 에서 별도의 auth 인증하는 추가로직이 필요하고 공식문서를 참고하시거나 이 포스팅의 마지막 하단에 추가내용을 확인하시면 될 것 같습니다.

 

 

 

 

 

먼저 결과 영상부터 보면 다음과 같습니다.

 

 

 

 

 

프로젝트가 파이어베이스와 연결이 되어있고 다음과 같이 auth  관련 디펜던시가 추가되어있는 상태여야 합니다.

또한, 파이어베이스 콘솔에서 인증 방법 선택에서 전화번호인증을 on 해주어야함을 잊지마세염.

implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    implementation 'com.google.firebase:firebase-auth:19.3.2'
    implementation 'com.google.firebase:firebase-auth-ktx:19.3.2'
    implementation 'com.google.firebase:firebase-messaging:20.2.4'
    implementation 'com.google.firebase:firebase-storage:19.2.0'
    implementation 'com.google.firebase:firebase-storage-ktx:19.2.0'
    implementation 'com.google.firebase:firebase-database:19.4.0'
    implementation 'com.google.firebase:firebase-database-ktx:19.4.0'

 

그리고 SHA 지문도 등록해줘야합니다

 

 

 

밑은 구현해야하는 전화번호 화면이었습니다. 처음에는 인증번호 입력 칸이 disabled 되어 있고 휴대폰번호 입력 후 인증요청을 하면 성공적으로 전화번호 인증코드 수신이 완료되면 인증번호 입력칸이 abled 되어서 칠 수 있게 했습니다. (참고로 사진상으로는 순서가 바뀌어있습니다.)

 

추가로, 전화번호 인증요청은 4번 이상정도 계속 요청하면 몇시간이나 하루동안은 인증요청이 안됩니다. (장난 요청으로 취급해서 구글에서 차단해버리기 때문이죠) 그래서 실제번호로 인증요청 테스트할때 조심하셔야합니다. 테스트 번호로 하는 방법도 문서에 작성되어있습니다.

테스트 번호 예제(전화번호와 인증코드를 미리 세팅할 수 있다.)

 

 

구현한 화면과 로직

 

 

 

[먼저 액티비티단 코드입니다. 이 액티비티에서 문자열을 따로 strings.xml로 빼지는 않았었네요.]

 

1. callbacks 가 전화번호 인증을 수신하는 객체입니다. 저 같은 경우 이 화면에 오기전에 이미 파이어베이스 이메일 로그인이 된 상태이므로 onCodeSent()로 로그인 하는 추가 로직 없이 onVertificationCompleted() 를 통해 바로 SMS 인증번호를 받을 수 있었습니다.

 

추가적으로 설명하면 

onVerificationCompleted() 은 번호인증 혹은 기타 다른 인증(구글로그인, 이메일로그인 등) 끝난 상태

onVerificationFailed() 은 번호인증 실패 상태

onCodeSent() 은 전화번호는 확인 되었으나 인증코드를 입력해야 하는 상태

입니다.

2. initViewModelCallback() 에서 requestAuth 를 observe하는 부분이 인증번호 요청을 눌렀을때입니다. 구글 파이어베이스 전화번호 인증은 전세계에서 사용하기 때문에 앞에 국가번호가 붙습니다. 이를 사용자가 직접입력하게 해도 되지만 전 편의성을 위해 사용자는 10-1234-5567 식으로 입력하게 안내하고 제가 앞에 +82를 붙여서 전화번호 요청을 했습니다.

 

3. 

package com.mtjin.nomoneytrip.views.phone

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.Observer
import com.google.firebase.FirebaseException
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.PhoneAuthProvider
import com.mtjin.nomoneytrip.R
import com.mtjin.nomoneytrip.base.BaseActivity
import com.mtjin.nomoneytrip.databinding.ActivityPhoneAuthBinding
import com.mtjin.nomoneytrip.views.login.LoginActivity
import com.mtjin.nomoneytrip.views.main.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.util.concurrent.TimeUnit

class PhoneAuthActivity : BaseActivity<ActivityPhoneAuthBinding>(R.layout.activity_phone_auth) {
    private val viewModel: PhoneAuthViewModel by viewModel()
    private val callbacks by lazy {
        object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {

            override fun onVerificationCompleted(phoneAuth: PhoneAuthCredential) {
                showToast("인증코드가 전송되었습니다. 60초 이내에 입력해주세요 :)")
                viewModel.authNum = phoneAuth.smsCode.toString()
                binding.etEnterCode.isEnabled = true
                binding.tvAuthNext.isEnabled = true
                binding.etPhone.isEnabled = true
                viewModel.updateAuthState(true)
            }

            override fun onVerificationFailed(p0: FirebaseException) {
                showToast("인증실패")
                binding.etPhone.isEnabled = true
                Log.d(com.mtjin.nomoneytrip.utils.TAG, p0.toString())
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.vm = viewModel
        initViewModelCallback()
    }

    private fun initViewModelCallback() {
        with(viewModel) {
            requestAuth.observe(this@PhoneAuthActivity, Observer {
                Log.d(com.mtjin.nomoneytrip.utils.TAG, viewModel.etPhoneNum.toString())
                if (it) {
                    PhoneAuthProvider.getInstance().verifyPhoneNumber(
                        "+82" + viewModel.etPhoneNum.value.toString(), // Phone number to verify
                        60, // Timeout duration
                        TimeUnit.SECONDS, // Unit of timeout
                        this@PhoneAuthActivity, // Activity (for callback binding)
                        callbacks
                    ) // OnVerificationStateChangedCallbacks
                    binding.etPhone.isEnabled = false
                } else {
                    binding.etPhone.error = getString(R.string.please_enter_phone_err)
                }
            })

            resultAuthUser.observe(this@PhoneAuthActivity, Observer { success ->
                if (!success) {
                    showToast("인증실패")
                } else {
                    startActivity(Intent(this@PhoneAuthActivity, MainActivity::class.java))
                    showToast("인증성공")
                    finish()
                }
            })

            backClick.observe(this@PhoneAuthActivity, Observer {
                startActivity(Intent(this@PhoneAuthActivity, LoginActivity::class.java))
                finish()
            })
        }
    }

}

 

 

 

[다음은 ViewModel 코드입니다.]

네 평범한 뷰모델 코드입니다. 레포지토리 부분은 따로 다루지 않겠습니다.

package com.mtjin.nomoneytrip.views.phone

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mtjin.nomoneytrip.base.BaseViewModel
import com.mtjin.nomoneytrip.data.phone.source.PhoneAuthRepository
import com.mtjin.nomoneytrip.utils.SingleLiveEvent
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers

class PhoneAuthViewModel(private val repository: PhoneAuthRepository) : BaseViewModel() {
    lateinit var authNum: String //문자로온 인증번호
    var tel: String = ""
    val etPhoneNum = MutableLiveData<String>("")
    val etAuthNum = MutableLiveData<String>("")
    private val _requestAuth = SingleLiveEvent<Boolean>()
    private val _authState = MutableLiveData<Boolean>() //인증번호 요청 했는지 유무
    private val _resultAuthUser = MutableLiveData<Boolean>()

    val requestAuth: LiveData<Boolean> get() = _requestAuth
    val authState: LiveData<Boolean> get() = _authState
    val resultAuthUser: LiveData<Boolean> get() = _resultAuthUser

    fun requestAuth() {
        _requestAuth.value = !etPhoneNum.value.isNullOrBlank()
    }

    fun updateAuthState(boolean: Boolean) {
        _authState.value = boolean
        tel = etPhoneNum.value.toString()
    }

    fun authUser() {
        if (this::authNum.isInitialized && authNum == etAuthNum.value.toString()) {
            updateUserTel()
        } else {
            _resultAuthUser.value = false
        }
    }

    private fun updateUserTel() {
        compositeDisposable.add(
            repository.updateUserTel(tel = tel)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showProgress() }
                .doAfterTerminate { hideProgress() }
                .subscribeBy(
                    onError = { _resultAuthUser.value = false },
                    onComplete = { _resultAuthUser.value = true }
                )
        )
    }
}

 

 

 

[마지막으로 xml 코드입니다. 데이터 바인딩처리가 되어있습니다.]

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/tools">

    <data>

        <import type="android.view.View" />

        <variable
            name="vm"
            type="com.mtjin.nomoneytrip.views.phone.PhoneAuthViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_16dp"
            android:layout_marginTop="@dimen/margin_32dp"
            android:onClick="@{()->vm.onBackClick()}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_back_title" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/text_self_auth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_28dp"
            android:layout_marginTop="@dimen/margin_16dp"
            android:fontFamily="@font/goyang_ilsan_regular"
            android:text="@string/self_auth_text"
            android:textColor="@color/colorBlack2D2D"
            android:textSize="@dimen/text_size_20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_back" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/text_self_auth_need"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_12dp"
            android:text="@string/need_self_auth_for_app_text"
            android:textColor="@color/colorOrangeF79256"
            android:textSize="@dimen/text_size_14sp"
            app:layout_constraintStart_toStartOf="@id/text_self_auth"
            app:layout_constraintTop_toBottomOf="@id/text_self_auth" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/text_phone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_36dp"
            android:text="@string/enter_phone_text"
            android:textColor="@color/colorGray8C8C"
            android:textSize="@dimen/text_size_12sp"
            app:layout_constraintStart_toStartOf="@id/text_self_auth"
            app:layout_constraintTop_toBottomOf="@id/text_self_auth_need" />

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/et_phone"
            android:layout_width="0dp"
            android:layout_height="@dimen/height_40dp"
            android:layout_marginTop="@dimen/margin_6dp"
            android:layout_marginEnd="@dimen/margin_16dp"
            android:background="@drawable/bg_edit_stroke_gray_c8c8_radius_2dp"
            android:inputType="numberDecimal"
            android:maxLength="10"
            android:padding="@dimen/padding_8dp"
            android:singleLine="true"
            android:text="@={vm.etPhoneNum}"
            app:layout_constraintEnd_toStartOf="@id/tv_auth_request"
            app:layout_constraintStart_toStartOf="@id/text_self_auth"
            app:layout_constraintTop_toBottomOf="@id/text_phone" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tv_auth_request"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="@dimen/margin_28dp"
            android:onClick="@{()->vm.requestAuth()}"
            android:text="@string/auth_request_text"
            android:textColor="@color/colorOrangeF79256"
            android:textSize="@dimen/text_size_14sp"
            app:layout_constraintBottom_toBottomOf="@id/et_phone"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/et_phone"
            app:layout_constraintTop_toTopOf="@id/et_phone" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/text_enter_phone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_16dp"
            android:text="@string/enter_auth_num_text"
            android:textColor="@color/colorGray8C8C"
            android:textSize="@dimen/text_size_12sp"
            app:layout_constraintStart_toStartOf="@id/text_self_auth"
            app:layout_constraintTop_toBottomOf="@id/et_phone" />

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/et_enter_code"
            android:layout_width="0dp"
            android:layout_height="@dimen/height_40dp"
            android:layout_marginTop="@dimen/margin_6dp"
            android:layout_marginEnd="@dimen/margin_28dp"
            android:background="@drawable/bg_edit_solid_gray_f4f4_radius_2dp"
            android:enabled="false"
            android:inputType="numberDecimal"
            android:padding="@dimen/padding_8dp"
            android:singleLine="true"
            android:text="@={vm.etAuthNum}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="@id/text_self_auth"
            app:layout_constraintTop_toBottomOf="@id/text_enter_phone"
            bind:onEnterBackground="@{vm.authState}" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tv_retry_auth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_4dp"
            android:onClick="@{()->vm.requestAuth()}"
            android:text="@string/retry_request_auth_phone_text"
            android:textColor="@color/colorOrangeF79256"
            android:textSize="@dimen/text_size_12sp"
            app:layout_constraintStart_toStartOf="@id/text_self_auth"
            app:layout_constraintTop_toBottomOf="@id/et_enter_code" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tv_auth_next"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_28dp"
            android:layout_marginTop="@dimen/margin_32dp"
            android:layout_marginEnd="@dimen/margin_28dp"
            android:background="@drawable/bg_btn_solid_gray_c8c8_radius_8dp"
            android:enabled="false"
            android:gravity="center"
            android:onClick="@{()->vm.authUser()}"
            android:paddingTop="@dimen/padding_11dp"
            android:paddingBottom="@dimen/padding_12dp"
            android:text="@string/try_auth_text"
            android:textColor="@color/colorWhiteFDFD"
            android:textSize="@dimen/text_size_14sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_retry_auth"
            bind:onRequestBackground="@{vm.authState}" />

        <ProgressBar
            android:id="@+id/pb_loading"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:visibility="@{vm.isLoading() ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

파이어베이스 덕분에 쉽게 전화번호 인증을 구현할 수 있었습니다. 그리고 인증번호 요청 메시지도 본인이 커스텀할 수 있습니다. (전 기본은 영어로되어있는데 한국어로만 변경해주었습니다.)

 

 

 


[빠진 추가내용]

https://console.cloud.google.com/apis/library/androidcheck.googleapis.com

 

Google Cloud Platform

하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

accounts.google.com

다음 사이트에서 내 프로젝트의 Android Device Verificaiton을 사용 수락해야합니다.

 

 


[파이어베이스 인증이 안되어있는 경우 onCodeSent() 를 사용한 전화번호 인증 및 로그인]

 

파이어베이스 로그인 인증이 안되어있는 경우 위에서 구현한 것 처럼 onVerificationCompleted() 로 바로 인증번호를 알 수 없습니다. 이를 위해 onCodeSent() 와 인증하는 로직을 더해서 구현해야합니다.

단계를 먼저 간단히 설명하면 

 

1. startPhoneNumberVerification() 를 통해 전화번호 인증코드를 요청한다.

2. Callbacks의 onCodeSent()에서 인증ID(verificationId) 와 토큰(token, 재전송시 사용)을 얻고 나중에 사용하기 위해 변수에 저장해놓는다. 

3. 2에서 받은 인증ID와 메시지로 받은 인증코드를 사용해(매칭해) PhoneAuthCredential을 생성하고 verifyPhoneNumberWithCode() 를 통해 인증 로그인을 시도한다. 

 

또한 참고로 전화번호 인증은 한 기기당 할당량이 있기 때문에 여러번 테스트가 불가합니다.(문자가안옴) 

그래서 파이어베이스 인증에서 전화를 클릭 후 밑과 같이 테스트 전화번호를 입력해서 테스트하길 권장드립니다. 전화번호와 보낼 인증코드를 미리적으면 됩니다. 

이 번호를 사용 시 문자메시지는 안오고 그냥 저 번호의 인증코드를 입력하면 됩니다. (문자메시지 안온다고 에러가아니고 원래 테스트번호이기 때문에 안오는 겁니다. 주의!!)

 

 

 

코드에 주석을 적어놨고 이해하실 수 있으리라 믿습니다. ㅎㅎ

[레포지토리 코드]

https://github.com/OnzeGgaaziFlow/EnvironmentMate-Android/blob/main/app/src/main/java/com/mtjin/envmate/views/sign_up/phone_auth/PhoneAuthActivity.kt

[디자인]

 

[간단요약버전]

[Activity]

package com.mtjin.envmate.views.sign_up.phone_auth

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import com.google.firebase.FirebaseException
import com.google.firebase.FirebaseTooManyRequestsException
import com.google.firebase.auth.*
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.mtjin.envmate.R
import com.mtjin.envmate.base.BaseActivity
import com.mtjin.envmate.databinding.ActivityPhoneAuthBinding
import com.mtjin.envmate.utils.TAG
import com.mtjin.envmate.views.sign_up.user_info.UserInfoActivity
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.TimeUnit

@AndroidEntryPoint
class PhoneAuthActivity : BaseActivity<ActivityPhoneAuthBinding>(R.layout.activity_phone_auth) {

    private lateinit var auth: FirebaseAuth
    private var storedVerificationId = ""
    private var resendToken: PhoneAuthProvider.ForceResendingToken? = null
    private val viewModel: PhoneAuthViewModel by viewModels()

    private val callbacks by lazy {
        object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {

            // 번호인증 혹은 기타 다른 인증(구글로그인, 이메일로그인 등) 끝난 상태
            override fun onVerificationCompleted(credential: PhoneAuthCredential) {
                // This callback will be invoked in two situations:
                // 1 - Instant verification. In some cases the phone number can be instantly
                //     verified without needing to send or enter a verification code.
                // 2 - Auto-retrieval. On some devices Google Play services can automatically
                //     detect the incoming verification SMS and perform verification without
                //     user action.
                showToast("인증코드가 전송되었습니다. 90초 이내에 입력해주세요 :)")
                verifyPhoneNumberWithCode(credential)
            }

            // 번호인증 실패 상태
            override fun onVerificationFailed(e: FirebaseException) {
                // This callback is invoked in an invalid request for verification is made,
                // for instance if the the phone number format is not valid.
                Log.w(TAG, "onVerificationFailed", e)
                if (e is FirebaseAuthInvalidCredentialsException) {
                    // Invalid request
                } else if (e is FirebaseTooManyRequestsException) {
                    // The SMS quota for the project has been exceeded
                }
                showToast("인증실패")
            }

            // 전화번호는 확인 되었으나 인증코드를 입력해야 하는 상태
            override fun onCodeSent(
                verificationId: String,
                token: PhoneAuthProvider.ForceResendingToken
            ) {
                // The SMS verification code has been sent to the provided phone number, we
                // now need to ask the user to enter the code and then construct a credential
                // by combining the code with a verification ID.
                Log.d(TAG, "onCodeSent:$verificationId")
                // Save verification ID and resending token so we can use them later
                storedVerificationId = verificationId // verificationId 와 전화번호인증코드 매칭해서 인증하는데 사용예정
                resendToken = token
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Initialize Firebase Auth
        auth = Firebase.auth
        binding.vm = viewModel
        initViewModelCallback()
    }

    private fun initViewModelCallback() {
        with(viewModel) {
            requestPhoneAuth.observe(this@PhoneAuthActivity, Observer { // 인증번호 요청
                if (it) {
                    viewModel.phoneAuthNum = ""
                    startPhoneNumberVerification(
                        "+82" + viewModel.etPhoneNum.value.toString().substring(1)
                    )
                } else {
                    showToast("전화번호를 입력해주세요")
                }
            })

            requestResendPhoneAuth.observe(this@PhoneAuthActivity, Observer { // 인증번호 재요청
                if (it) {
                    viewModel.phoneAuthNum = ""
                    resendVerificationCode(
                        "+82" + viewModel.etPhoneNum.value.toString().substring(1)
                        , resendToken
                    )
                } else {
                    showToast("전화번호를 입력해주세요")
                }
            })

            authComplete.observe(this@PhoneAuthActivity, Observer { // 인증완료 버튼 클릭 시
                // 휴대폰 인증번호로 인증 및 로그인 실행
                // onCodeSent() 에서 받은 vertificationID 와 문자메시지로 전송한 인증코드값으로 Credintial 만든 후 인증 시도
                val phoneCredential =
                    PhoneAuthProvider.getCredential(
                        storedVerificationId,
                        viewModel.etAuthNum.value!!
                    )
                verifyPhoneNumberWithCode(phoneCredential)
            })
        }
    }

    // 전화번호 인증코드 요청
    private fun startPhoneNumberVerification(phoneNumber: String) {
        val options = PhoneAuthOptions.newBuilder(auth)
            .setPhoneNumber(phoneNumber)       // Phone number to verify
            .setTimeout(90L, TimeUnit.SECONDS) // Timeout and unit
            .setActivity(this)                 // Activity (for callback binding)
            .setCallbacks(callbacks)          // OnVerificationStateChangedCallbacks
            .build()
        PhoneAuthProvider.verifyPhoneNumber(options)

        binding.phoneAuthBtnAuth.run {
            text = "재전송"
            setTextColor(
                ContextCompat.getColor(
                    this@PhoneAuthActivity, R.color.dark_gray_333333
                )
            )
            background = ContextCompat.getDrawable(
                this@PhoneAuthActivity, R.drawable.bg_btn_stroke_dark_gray_333333_radius_8dp
            )
        }
    }

    // 전화번호 인증코드 재요청
    private fun resendVerificationCode(
        phoneNumber: String,
        token: PhoneAuthProvider.ForceResendingToken?
    ) {
        val optionsBuilder = PhoneAuthOptions.newBuilder(auth)
            .setPhoneNumber(phoneNumber)       // Phone number to verify
            .setTimeout(90L, TimeUnit.SECONDS) // Timeout and unit
            .setActivity(this)                 // Activity (for callback binding)
            .setCallbacks(callbacks)          // OnVerificationStateChangedCallbacks
        if (token != null) {
            optionsBuilder.setForceResendingToken(token) // callback's ForceResendingToken
        }
        PhoneAuthProvider.verifyPhoneNumber(optionsBuilder.build())
    }

    // 전화번호 인증 실행 (onCodeSent() 에서 받은 vertificationID 와
    // 문자로 받은 인증코드로 생성한 PhoneAuthCredential 사용)
    private fun verifyPhoneNumberWithCode(phoneAuthCredential: PhoneAuthCredential) {
        Firebase.auth.signInWithCredential(phoneAuthCredential)
            .addOnCompleteListener(this@PhoneAuthActivity) { task ->
                if (task.isSuccessful) {
                    Log.d(TAG, "signInWithCredential:success")
                    showToast("인증 성공")
                    startActivity(
                        Intent(this@PhoneAuthActivity, UserInfoActivity::class.java)
                    )
                } else {
                    binding.phoneAuthTvAuthNum.text =
                        getString(R.string.auth_num_wrong_text)
                    binding.phoneAuthTvAuthNum.setTextColor(
                        ContextCompat.getColor(this@PhoneAuthActivity, R.color.red_FF5050)
                    )
                }
            }
    }

}

 

[ViewModel]

package com.mtjin.envmate.views.sign_up.phone_auth

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mtjin.envmate.base.BaseViewModel
import com.mtjin.envmate.utils.SingleLiveEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class PhoneAuthViewModel @Inject constructor() : BaseViewModel() {
    var phoneAuthNum: String = ""
    var isResendPhoneAuth: Boolean = false
    val etPhoneNum = MutableLiveData<String>("")
    val etAuthNum = MutableLiveData<String>("")

    private val _requestPhoneAuth = MutableLiveData<Boolean>()
    private val _requestResendPhoneAuth = MutableLiveData<Boolean>()
    private val _authComplete = SingleLiveEvent<Unit>()

    val requestPhoneAuth: LiveData<Boolean> get() = _requestPhoneAuth
    val requestResendPhoneAuth: LiveData<Boolean> get() = _requestResendPhoneAuth
    val authComplete: LiveData<Unit> get() = _authComplete

    fun requestPhoneAuth() {
        if (!isResendPhoneAuth) { //첫 폰인증
            _requestPhoneAuth.value = !etPhoneNum.value.isNullOrBlank()
        } else { //재시도
            _requestResendPhoneAuth.value = !etPhoneNum.value.isNullOrBlank()
        }
    }

    fun authComplete() {
        _authComplete.call()
    }
}

[이후 필요한 로직 추가한 버전]

[Activity]

package com.mtjin.envmate.views.sign_up.phone_auth

import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import com.google.firebase.FirebaseException
import com.google.firebase.FirebaseTooManyRequestsException
import com.google.firebase.auth.*
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.mtjin.envmate.R
import com.mtjin.envmate.base.BaseActivity
import com.mtjin.envmate.databinding.ActivityPhoneAuthBinding
import com.mtjin.envmate.utils.TAG
import com.mtjin.envmate.utils.UserInfo
import com.mtjin.envmate.views.sign_up.user_info.UserInfoActivity
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.TimeUnit

@AndroidEntryPoint
class PhoneAuthActivity : BaseActivity<ActivityPhoneAuthBinding>(R.layout.activity_phone_auth) {

    private lateinit var auth: FirebaseAuth
    private var storedVerificationId = ""
    private var resendToken: PhoneAuthProvider.ForceResendingToken? = null
    private val viewModel: PhoneAuthViewModel by viewModels()

    private val callbacks by lazy {
        object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {

            // 번호인증 혹은 기타 다른 인증(구글로그인, 이메일로그인 등) 끝난 상태
            override fun onVerificationCompleted(credential: PhoneAuthCredential) {
                // This callback will be invoked in two situations:
                // 1 - Instant verification. In some cases the phone number can be instantly
                //     verified without needing to send or enter a verification code.
                // 2 - Auto-retrieval. On some devices Google Play services can automatically
                //     detect the incoming verification SMS and perform verification without
                //     user action.
                showToast("인증코드가 전송되었습니다. 90초 이내에 입력해주세요 :)")
                UserInfo.phoneAuthNum = credential.smsCode.toString()
                binding.phoneAuthEtAuthNum.setText(credential.smsCode.toString())
                binding.phoneAuthEtAuthNum.isEnabled = false
                Handler(Looper.getMainLooper()).postDelayed({
                    verifyPhoneNumberWithCode(credential)
                }, 1000)
            }

            // 번호인증 실패 상태
            override fun onVerificationFailed(e: FirebaseException) {
                // This callback is invoked in an invalid request for verification is made,
                // for instance if the the phone number format is not valid.
                Log.w(TAG, "onVerificationFailed", e)
                if (e is FirebaseAuthInvalidCredentialsException) {
                    // Invalid request
                } else if (e is FirebaseTooManyRequestsException) {
                    // The SMS quota for the project has been exceeded
                }
                showToast("인증실패")
            }

            // 전화번호는 확인 되었으나 인증코드를 입력해야 하는 상태
            override fun onCodeSent(
                verificationId: String,
                token: PhoneAuthProvider.ForceResendingToken
            ) {
                // The SMS verification code has been sent to the provided phone number, we
                // now need to ask the user to enter the code and then construct a credential
                // by combining the code with a verification ID.
                Log.d(TAG, "onCodeSent:$verificationId")
                // Save verification ID and resending token so we can use them later
                storedVerificationId = verificationId // verificationId 와 전화번호인증코드 매칭해서 인증하는데 사용예정
                resendToken = token
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Initialize Firebase Auth
        auth = Firebase.auth
        binding.vm = viewModel
        initViewModelCallback()
    }

    private fun initViewModelCallback() {
        with(viewModel) {
            requestPhoneAuth.observe(this@PhoneAuthActivity, Observer { // 인증번호 요청
                UserInfo.tel = viewModel.etPhoneNum.value.toString() // 전화번호 저장
                if (it) {
                    startPhoneNumberVerification(
                        "+82" + viewModel.etPhoneNum.value.toString().substring(1)
                    )
                } else {
                    showToast("전화번호를 입력해주세요")
                }
            })

            requestResendPhoneAuth.observe(this@PhoneAuthActivity, Observer { // 인증번호 재요청
                if (it) {
                    resendVerificationCode(
                        "+82" + viewModel.etPhoneNum.value.toString().substring(1)
                        , resendToken
                    )
                } else {
                    showToast("전화번호를 입력해주세요")
                }
            })

            authComplete.observe(this@PhoneAuthActivity, Observer { // 인증완료 버튼 클릭 시
                // 휴대폰 인증번호로 인증 및 로그인 실행
                // onCodeSent() 에서 받은 vertificationID 와 문자메시지로 전송한 인증코드값으로 Credintial 만든 후 인증 시도
                try {
                    val phoneCredential =
                        PhoneAuthProvider.getCredential(
                            storedVerificationId,
                            viewModel.etAuthNum.value!!
                        )
                    verifyPhoneNumberWithCode(phoneCredential)
                } catch (e: Exception) {
                    Log.d(TAG, e.toString())
                }
            })
        }
    }

    // 전화번호 인증코드 요청
    private fun startPhoneNumberVerification(phoneNumber: String) {
        val options = PhoneAuthOptions.newBuilder(auth)
            .setPhoneNumber(phoneNumber)       // Phone number to verify
            .setTimeout(90L, TimeUnit.SECONDS) // Timeout and unit
            .setActivity(this)                 // Activity (for callback binding)
            .setCallbacks(callbacks)          // OnVerificationStateChangedCallbacks
            .build()
        PhoneAuthProvider.verifyPhoneNumber(options)

        binding.phoneAuthBtnAuth.run {
            text = "재전송"
            setTextColor(
                ContextCompat.getColor(
                    this@PhoneAuthActivity, R.color.dark_gray_333333
                )
            )
            background = ContextCompat.getDrawable(
                this@PhoneAuthActivity, R.drawable.bg_btn_stroke_dark_gray_333333_radius_8dp
            )
        }
    }

    // 전화번호 인증코드 재요청
    private fun resendVerificationCode(
        phoneNumber: String,
        token: PhoneAuthProvider.ForceResendingToken?
    ) {
        val optionsBuilder = PhoneAuthOptions.newBuilder(auth)
            .setPhoneNumber(phoneNumber)       // Phone number to verify
            .setTimeout(90L, TimeUnit.SECONDS) // Timeout and unit
            .setActivity(this)                 // Activity (for callback binding)
            .setCallbacks(callbacks)          // OnVerificationStateChangedCallbacks
        if (token != null) {
            optionsBuilder.setForceResendingToken(token) // callback's ForceResendingToken
        }
        PhoneAuthProvider.verifyPhoneNumber(optionsBuilder.build())
    }

    // 전화번호 인증 실행 (onCodeSent() 에서 받은 vertificationID 와
    // 문자로 받은 인증코드로 생성한 PhoneAuthCredential 사용)
    private fun verifyPhoneNumberWithCode(phoneAuthCredential: PhoneAuthCredential) {
        UserInfo.tel = binding.phoneAuthEtPhoneNum.text.toString()
        if (UserInfo.tel.isNotBlank() && UserInfo.phoneAuthNum.isNotBlank() &&
            (UserInfo.tel == binding.phoneAuthEtPhoneNum.text.toString() && UserInfo.phoneAuthNum == binding.phoneAuthEtAuthNum.text.toString())
        ) { // 이전에  인증한 번호와 인증번호인 경우
            showToast("인증 성공")
            UserInfo.tel = binding.phoneAuthEtPhoneNum.text.toString()
            startActivity(Intent(this@PhoneAuthActivity, UserInfoActivity::class.java))
            return
        }
        Firebase.auth.signInWithCredential(phoneAuthCredential)
            .addOnCompleteListener(this@PhoneAuthActivity) { task ->
                if (task.isSuccessful) {
                    showToast("인증 성공")
                    UserInfo.tel = binding.phoneAuthEtPhoneNum.text.toString()
                    binding.phoneAuthEtAuthNum.isEnabled = true
                    startActivity(
                        Intent(this@PhoneAuthActivity, UserInfoActivity::class.java)
                    )
                } else {
                    binding.phoneAuthTvAuthNum.text =
                        getString(R.string.auth_num_wrong_text)
                    binding.phoneAuthTvAuthNum.setTextColor(
                        ContextCompat.getColor(this@PhoneAuthActivity, R.color.red_FF5050)
                    )
                    binding.phoneAuthEtAuthNum.isEnabled = true
                }
            }
    }

}

 

[ViewModel]

package com.mtjin.envmate.views.sign_up.phone_auth

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mtjin.envmate.base.BaseViewModel
import com.mtjin.envmate.utils.SingleLiveEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class PhoneAuthViewModel @Inject constructor() : BaseViewModel() {
    var isResendPhoneAuth: Boolean = false

    val etPhoneNum = MutableLiveData<String>("")
    val etAuthNum = MutableLiveData<String>("")

    private val _requestPhoneAuth = MutableLiveData<Boolean>()
    private val _requestResendPhoneAuth = MutableLiveData<Boolean>()
    private val _authComplete = SingleLiveEvent<Unit>()

    val requestPhoneAuth: LiveData<Boolean> get() = _requestPhoneAuth
    val requestResendPhoneAuth: LiveData<Boolean> get() = _requestResendPhoneAuth
    val authComplete: LiveData<Unit> get() = _authComplete

    fun requestPhoneAuth() {
        if (!isResendPhoneAuth) { //첫 폰인증
            _requestPhoneAuth.value = !etPhoneNum.value.isNullOrBlank()
            //isResendPhoneAuth = true
        } else { //재시도
            _requestResendPhoneAuth.value = !etPhoneNum.value.isNullOrBlank()
        }
    }

    fun authComplete() {
        _authComplete.call()
    }

}

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

728x90
Comments