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

[WIP] move firebase auth calls to ViewModels #1459

Draft
wants to merge 9 commits into
base: compose
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
package com.google.firebase.quickstart.auth.kotlin

import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.google.firebase.auth.EmailAuthProvider
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.google.firebase.quickstart.auth.R
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.firebase.quickstart.auth.databinding.FragmentAnonymousAuthBinding
import kotlinx.coroutines.launch

class AnonymousAuthFragment : BaseFragment() {
private var _binding: FragmentAnonymousAuthBinding? = null
private val binding: FragmentAnonymousAuthBinding
get() = _binding!!

private lateinit var auth: FirebaseAuth
private val viewModel by viewModels<AnonymousAuthViewModel>()

override fun onCreateView(
inflater: LayoutInflater,
Expand All @@ -35,134 +31,50 @@ class AnonymousAuthFragment : BaseFragment() {
super.onViewCreated(view, savedInstanceState)
setProgressBar(binding.progressBar)

// Initialize Firebase Auth
auth = Firebase.auth

// Click listeners
binding.buttonAnonymousSignIn.setOnClickListener {
signInAnonymously()
viewModel.signInAnonymously()
}

binding.buttonAnonymousSignOut.setOnClickListener {
signOut()
}
binding.buttonLinkAccount.setOnClickListener {
linkAccount()
viewModel.signOut()
}
}

override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
}

private fun signInAnonymously() {
showProgressBar()
auth.signInAnonymously()
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInAnonymously:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInAnonymously:failure", task.exception)
Toast.makeText(context, "Authentication failed.",
Toast.LENGTH_SHORT).show()
updateUI(null)
}

hideProgressBar()
}
}

private fun signOut() {
auth.signOut()
updateUI(null)
}
binding.buttonLinkAccount.setOnClickListener {
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()

private fun linkAccount() {
// Make sure form is valid
if (!validateLinkForm()) {
return
viewModel.linkAccount(email, password)
}

// Get email and password from the form
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
// Handle errors
binding.fieldEmail.error = uiState.emailError
binding.fieldPassword.error = uiState.passwordError

// Create EmailAuthCredential with email and password
val credential = EmailAuthProvider.getCredential(email, password)
// Display texts
binding.anonymousStatusId.text = uiState.userId
binding.anonymousStatusEmail.text = uiState.userEmail

// Link the anonymous user to the email credential
showProgressBar()

auth.currentUser!!.linkWithCredential(credential)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
Log.d(TAG, "linkWithCredential:success")
val user = task.result?.user
updateUI(user)
// Toggle progress bar
if (uiState.isProgressBarVisible) {
showProgressBar()
} else {
Log.w(TAG, "linkWithCredential:failure", task.exception)
Toast.makeText(context, "Authentication failed.",
Toast.LENGTH_SHORT).show()
updateUI(null)
hideProgressBar()
}

hideProgressBar()
// Toggle button visibility
binding.buttonAnonymousSignIn.isEnabled = uiState.isSignInEnabled
binding.buttonLinkAccount.isEnabled = uiState.isLinkAccountEnabled
binding.buttonAnonymousSignOut.isEnabled = uiState.isSignOutEnabled
}
}

private fun validateLinkForm(): Boolean {
var valid = true

val email = binding.fieldEmail.text.toString()
if (TextUtils.isEmpty(email)) {
binding.fieldEmail.error = "Required."
valid = false
} else {
binding.fieldEmail.error = null
}

val password = binding.fieldPassword.text.toString()
if (TextUtils.isEmpty(password)) {
binding.fieldPassword.error = "Required."
valid = false
} else {
binding.fieldPassword.error = null
}

return valid
}

private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
val isSignedIn = user != null

// Status text
if (isSignedIn) {
binding.anonymousStatusId.text = getString(R.string.id_fmt, user!!.uid)
binding.anonymousStatusEmail.text = getString(R.string.email_fmt, user.email)
} else {
binding.anonymousStatusId.setText(R.string.signed_out)
binding.anonymousStatusEmail.text = null
}
}

// Button visibility
binding.buttonAnonymousSignIn.isEnabled = !isSignedIn
binding.buttonAnonymousSignOut.isEnabled = isSignedIn
binding.buttonLinkAccount.isEnabled = isSignedIn
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

companion object {
private const val TAG = "AnonymousAuth"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.google.firebase.quickstart.auth.kotlin

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.auth.EmailAuthProvider
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await

class AnonymousAuthViewModel(
private val firebaseAuth: FirebaseAuth = Firebase.auth
) : ViewModel() {

private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState

data class UiState(
var userId: String? = null,
var userEmail: String? = null,
var isSignInEnabled: Boolean = true,
var isSignOutEnabled: Boolean = false,
var isLinkAccountEnabled: Boolean = false,
var isProgressBarVisible: Boolean = false,
var emailError: String? = null,
var passwordError: String?= null,
)

init {
// Check if there's a user signed in and update UI State accordingly
val firebaseUser = firebaseAuth.currentUser
updateUiState(firebaseUser)
}

fun signInAnonymously() {
viewModelScope.launch {
toggleProgressbar(true)
try {
val authResult = firebaseAuth.signInAnonymously().await()
// Sign in was successful, update UI State with the signed-in user's information
Log.d(TAG, "signInAnonymously:success")
updateUiState(authResult.user)
} catch (e: Exception) {
Log.e(TAG, "signInAnonymously:failure", e)
// TODO(thatfiredev): Display "Authentication failed" snackbar
updateUiState(null)
} finally {
toggleProgressbar(false)
}
}
}

fun linkAccount(email: String, password: String) {
// Make sure entered data is valid
if (!validateEmailAndPassword(email, password)) {
return
}
val credential = EmailAuthProvider.getCredential(email, password)

viewModelScope.launch {
toggleProgressbar(true)
val firebaseUser = firebaseAuth.currentUser!!

try {
val authResult = firebaseUser.linkWithCredential(credential).await()
// Account Link was successful, update UI State with the signed-in user's information
Log.d(TAG, "linkWithCredential:success")
updateUiState(authResult.user)
} catch (e: Exception) {
Log.e(TAG, "linkWithCredential:failure", e)
// TODO(thatfiredev): Display "Authentication failed" snackbar
updateUiState(null)
} finally {
toggleProgressbar(false)
}
}
}

fun signOut() {
firebaseAuth.signOut()
updateUiState(null)
}

private fun updateUiState(user: FirebaseUser?) {
if (user == null) {
_uiState.update { currentUiState ->
currentUiState.copy(
userId = "Signed Out",
userEmail = null,
isSignInEnabled = true,
isSignOutEnabled = false,
isLinkAccountEnabled = false,
isProgressBarVisible = false
)
}
} else {
_uiState.update { currentUiState ->
currentUiState.copy(
userId = "User ID: ${user.uid}",
userEmail = "Email: ${user.email}",
isSignInEnabled = false,
isSignOutEnabled = true,
isLinkAccountEnabled = true,
isProgressBarVisible = false
)
}
}
}

private fun validateEmailAndPassword(email: String, password: String): Boolean {
var isValid = true

if (email.isBlank()) {
_uiState.update { it.copy(emailError = "Required.") }
isValid = false
} else {
_uiState.update { it.copy(emailError = null) }
}

if (password.isBlank()) {
_uiState.update { it.copy(passwordError = "Required.") }
isValid = false
} else {
_uiState.update { it.copy(passwordError = null) }
}

return isValid
}

private fun toggleProgressbar(isVisible: Boolean) {
_uiState.update { it.copy(isProgressBarVisible = isVisible) }
}

companion object {
const val TAG = "AnonymousAuthViewModel"
}
}
Loading