diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthFragment.kt index 5287e6bef..12152ac06 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthFragment.kt @@ -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() override fun onCreateView( inflater: LayoutInflater, @@ -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" - } } \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthViewModel.kt new file mode 100644 index 000000000..5003dd398 --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthViewModel.kt @@ -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 + + 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" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthFragment.kt index 0621e0ba6..f351fec29 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthFragment.kt @@ -5,31 +5,30 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.fragment.app.Fragment -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.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.firebase.quickstart.auth.databinding.FragmentCustomBinding +import kotlinx.coroutines.launch /** * Demonstrate Firebase Authentication using a custom minted token. For more information, see: * https://firebase.google.com/docs/auth/android/custom-auth */ class CustomAuthFragment : Fragment() { - - private lateinit var auth: FirebaseAuth - private var _binding: FragmentCustomBinding? = null private val binding: FragmentCustomBinding get() = _binding!! - private var customToken: String? = null + private val viewModel by viewModels() + private lateinit var tokenReceiver: TokenBroadcastReceiver - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentCustomBinding.inflate(inflater, container, false) return binding.root } @@ -38,76 +37,39 @@ class CustomAuthFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // Button click listeners - binding.buttonSignIn.setOnClickListener { startSignIn() } + binding.buttonSignIn.setOnClickListener { viewModel.startSignIn() } // Create token receiver (for demo purposes only) tokenReceiver = object : TokenBroadcastReceiver() { override fun onNewToken(token: String?) { Log.d(TAG, "onNewToken:$token") - setCustomToken(token.toString()) + viewModel.setCustomToken(token) } } - // Initialize Firebase Auth - auth = Firebase.auth - } - - override fun onStart() { - super.onStart() - // Check if user is signed in (non-null) and update UI accordingly. - val currentUser = auth.currentUser - updateUI(currentUser) - } - - override fun onResume() { - super.onResume() - requireActivity().registerReceiver(tokenReceiver, TokenBroadcastReceiver.filter) - } - - override fun onPause() { - super.onPause() - requireActivity().unregisterReceiver(tokenReceiver) - } - - private fun startSignIn() { - // Initiate sign in with custom token - customToken?.let { - auth.signInWithCustomToken(it) - .addOnCompleteListener(requireActivity()) { task -> - if (task.isSuccessful) { - // Sign in success, update UI with the signed-in user's information - Log.d(TAG, "signInWithCustomToken:success") - val user = auth.currentUser - updateUI(user) - } else { - // If sign in fails, display a message to the user. - Log.w(TAG, "signInWithCustomToken:failure", task.exception) - Toast.makeText(context, "Authentication failed.", - Toast.LENGTH_SHORT).show() - updateUI(null) - } - } - } - } + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + requireActivity().registerReceiver(tokenReceiver, TokenBroadcastReceiver.filter) + } - private fun updateUI(user: FirebaseUser?) { - if (user != null) { - binding.textSignInStatus.text = getString(R.string.custom_auth_signin_status_user, user.uid) - } else { - binding.textSignInStatus.text = getString(R.string.custom_auth_signin_status_failed) + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + requireActivity().unregisterReceiver(tokenReceiver) + } + }) + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + binding.buttonSignIn.isEnabled = uiState.isSignInEnabled + binding.textSignInStatus.text = uiState.signInStatus + binding.textTokenStatus.text = uiState.tokenStatus + } + } } } - private fun setCustomToken(token: String) { - customToken = token - - val status = "Token:$customToken" - - // Enable/disable sign-in button and show the token - binding.buttonSignIn.isEnabled = true - binding.textTokenStatus.text = status - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthViewModel.kt new file mode 100644 index 000000000..fe756f63e --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthViewModel.kt @@ -0,0 +1,81 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 CustomAuthViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + private var customToken: String? = null + + data class UiState( + var signInStatus: String = "", + var tokenStatus: String = "Token:null", + var isSignInEnabled: Boolean = false, + ) + + init { + // Check if user is signed in (non-null) and update UI accordingly. + val firebaseUser = firebaseAuth.currentUser + updateUiState(firebaseUser) + } + + fun setCustomToken(customToken: String?) { + this.customToken = customToken + + // Enable/disable sign-in button and show the token + _uiState.update { it.copy(isSignInEnabled = true, tokenStatus = "Token:$customToken") } + } + + fun startSignIn() { + customToken?.let { token -> + // Initiate sign in with custom token + viewModelScope.launch { + try { + val authResult = firebaseAuth.signInWithCustomToken(token).await() + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "signInWithCustomToken:success") + updateUiState(authResult.user) + } catch (e: Exception) { + // If sign in fails, display a message to the user. + Log.w(TAG, "signInWithCustomToken:failure", e) + // TODO(thatfiredev): Show "Authentication failed" snackbar + updateUiState(null) + } + } + } + } + + private fun updateUiState(user: FirebaseUser?) { + if (user != null) { + _uiState.update { currentUiState -> + currentUiState.copy( + signInStatus = "User ID: ${user.uid}" + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + signInStatus = "Error: sign in failed" + ) + } + } + } + + companion object { + private const val TAG = "CustomAuthViewModel" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordFragment.kt index 95db92d3c..cfd365166 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordFragment.kt @@ -1,31 +1,24 @@ package com.google.firebase.quickstart.auth.kotlin -import android.content.Intent 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 androidx.core.os.bundleOf -import androidx.navigation.fragment.findNavController -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseAuthMultiFactorException -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.core.view.isGone +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.firebase.quickstart.auth.databinding.FragmentEmailpasswordBinding +import kotlinx.coroutines.launch class EmailPasswordFragment : BaseFragment() { - - private lateinit var auth: FirebaseAuth - private var _binding: FragmentEmailpasswordBinding? = null private val binding: FragmentEmailpasswordBinding get() = _binding!! + private val viewModel by viewModels() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { _binding = FragmentEmailpasswordBinding.inflate(inflater, container, false) return binding.root @@ -41,200 +34,50 @@ class EmailPasswordFragment : BaseFragment() { emailSignInButton.setOnClickListener { val email = binding.fieldEmail.text.toString() val password = binding.fieldPassword.text.toString() - signIn(email, password) + + viewModel.signIn(email, password) } emailCreateAccountButton.setOnClickListener { val email = binding.fieldEmail.text.toString() val password = binding.fieldPassword.text.toString() - createAccount(email, password) - } - signOutButton.setOnClickListener { signOut() } - verifyEmailButton.setOnClickListener { sendEmailVerification() } - reloadButton.setOnClickListener { reload() } - } - // Initialize Firebase Auth - auth = Firebase.auth - } - - public override fun onStart() { - super.onStart() - // Check if user is signed in (non-null) and update UI accordingly. - val currentUser = auth.currentUser - if(currentUser != null){ - reload(); - } - } - - private fun createAccount(email: String, password: String) { - Log.d(TAG, "createAccount:$email") - if (!validateForm()) { - return + viewModel.createAccount(email, password) + } + signOutButton.setOnClickListener { viewModel.signOut() } + verifyEmailButton.setOnClickListener { viewModel.sendEmailVerification() } + reloadButton.setOnClickListener { viewModel.reload() } } - showProgressBar() - - auth.createUserWithEmailAndPassword(email, password) - .addOnCompleteListener(requireActivity()) { task -> - if (task.isSuccessful) { - // Sign in success, update UI with the signed-in user's information - Log.d(TAG, "createUserWithEmail:success") - val user = auth.currentUser - updateUI(user) - } else { - // If sign in fails, display a message to the user. - Log.w(TAG, "createUserWithEmail:failure", task.exception) - Toast.makeText(context, "Authentication failed.", - Toast.LENGTH_SHORT).show() - updateUI(null) - } - - hideProgressBar() - } - } - - private fun signIn(email: String, password: String) { - Log.d(TAG, "signIn:$email") - if (!validateForm()) { - return - } + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + // Handle errors + binding.fieldEmail.error = uiState.emailError + binding.fieldPassword.error = uiState.passwordError - showProgressBar() + // Display texts + binding.status.text = uiState.userId + binding.detail.text = uiState.userEmail - auth.signInWithEmailAndPassword(email, password) - .addOnCompleteListener(requireActivity()) { task -> - if (task.isSuccessful) { - // Sign in success, update UI with the signed-in user's information - Log.d(TAG, "signInWithEmail:success") - val user = auth.currentUser - updateUI(user) + // Toggle progress bar + if (uiState.isProgressBarVisible) { + showProgressBar() } else { - // If sign in fails, display a message to the user. - Log.w(TAG, "signInWithEmail:failure", task.exception) - Toast.makeText(context, "Authentication failed.", - Toast.LENGTH_SHORT).show() - updateUI(null) - checkForMultiFactorFailure(task.exception!!) + hideProgressBar() } - if (!task.isSuccessful) { - binding.status.setText(R.string.auth_failed) - } - hideProgressBar() + // Toggle button visibility + binding.verifyEmailButton.isGone = !uiState.isVerifyEmailVisible + binding.emailPasswordButtons.isGone = !uiState.isSignInEnabled + binding.emailPasswordFields.isGone = !uiState.isSignInEnabled + binding.signedInButtons.isGone = uiState.isSignInEnabled } - } - - private fun signOut() { - auth.signOut() - updateUI(null) - } - - private fun sendEmailVerification() { - // Disable button - binding.verifyEmailButton.isEnabled = false - - // Send verification email - val user = auth.currentUser!! - user.sendEmailVerification() - .addOnCompleteListener(requireActivity()) { task -> - // Re-enable button - binding.verifyEmailButton.isEnabled = true - - if (task.isSuccessful) { - Toast.makeText(context, - "Verification email sent to ${user.email} ", - Toast.LENGTH_SHORT).show() - } else { - Log.e(TAG, "sendEmailVerification", task.exception) - Toast.makeText(context, - "Failed to send verification email.", - Toast.LENGTH_SHORT).show() - } - } - } - - private fun reload() { - auth.currentUser!!.reload().addOnCompleteListener { task -> - if (task.isSuccessful) { - updateUI(auth.currentUser) - Toast.makeText(context, "Reload successful!", Toast.LENGTH_SHORT).show() - } else { - Log.e(TAG, "reload", task.exception) - Toast.makeText(context, "Failed to reload user.", Toast.LENGTH_SHORT).show() } } } - private fun validateForm(): 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() - if (user != null) { - binding.status.text = getString(R.string.emailpassword_status_fmt, - user.email, user.isEmailVerified) - binding.detail.text = getString(R.string.firebase_status_fmt, user.uid) - - binding.emailPasswordButtons.visibility = View.GONE - binding.emailPasswordFields.visibility = View.GONE - binding.signedInButtons.visibility = View.VISIBLE - - if (user.isEmailVerified) { - binding.verifyEmailButton.visibility = View.GONE - } else { - binding.verifyEmailButton.visibility = View.VISIBLE - } - } else { - binding.status.setText(R.string.signed_out) - binding.detail.text = null - - binding.emailPasswordButtons.visibility = View.VISIBLE - binding.emailPasswordFields.visibility = View.VISIBLE - binding.signedInButtons.visibility = View.GONE - } - } - - private fun checkForMultiFactorFailure(e: Exception) { - // Multi-factor authentication with SMS is currently only available for - // Google Cloud Identity Platform projects. For more information: - // https://cloud.google.com/identity-platform/docs/android/mfa - if (e is FirebaseAuthMultiFactorException) { - Log.w(TAG, "multiFactorFailure", e) - val resolver = e.resolver - val args = bundleOf( - MultiFactorSignInFragment.EXTRA_MFA_RESOLVER to resolver, - MultiFactorFragment.RESULT_NEEDS_MFA_SIGN_IN to true - ) - findNavController().navigate(R.id.action_emailpassword_to_mfa, args) - } - } - override fun onDestroyView() { super.onDestroyView() _binding = null } - - companion object { - private const val TAG = "EmailPassword" - private const val RC_MULTI_FACTOR = 9005 - } } diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordViewModel.kt new file mode 100644 index 000000000..b9f13b5eb --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordViewModel.kt @@ -0,0 +1,184 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 EmailPasswordViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + data class UiState( + var userId: String? = null, + var userEmail: String? = null, + var isSignInEnabled: Boolean = true, + var isCreateAccountEnabled: Boolean = false, + var isVerifyEmailVisible: Boolean = false, + var isReloadEnabled: 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 + firebaseUser?.let { reload() } + } + + fun signIn(email: String, password: String) { + if (!validateEmailAndPassword(email, password)) { + return + } + + Log.d(TAG, "signIn:$email") + + viewModelScope.launch { + toggleProgressbar(isVisible = true) + try { + val authResult = firebaseAuth.signInWithEmailAndPassword(email, password).await() + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "signInWithEmail:success") + updateUiState(authResult.user) + } catch (e: Exception) { + // If sign in fails, display a message to the user. + Log.e(TAG, "signInWithEmail:failure", e) + // TODO(thatfiredev): Show snackbar + updateUiState(null) + // TODO(thatfiredev): Check for multifactor failure once firebase/quickstart-android#1443 is fixed + + _uiState.update { it.copy(userId = "Authentication failed") } + } finally { + toggleProgressbar(isVisible = false) + } + } + } + + fun createAccount(email: String, password: String) { + if (!validateEmailAndPassword(email, password)) { + return + } + + Log.d(TAG, "createAccount:$email") + + viewModelScope.launch { + toggleProgressbar(isVisible = true) + try { + val authResult = firebaseAuth.createUserWithEmailAndPassword(email, password).await() + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "createUserWithEmail:success") + updateUiState(authResult.user) + } catch (e: Exception) { + // If account creation fails, display a message to the user. + Log.e(TAG, "createUserWithEmail:failure", e) + // TODO(thatfiredev): Show snackbar + updateUiState(null) + } finally { + toggleProgressbar(isVisible = false) + } + } + } + + fun sendEmailVerification() { + // Disable button + _uiState.update { it.copy(isVerifyEmailVisible = false) } + + viewModelScope.launch { + try { + // Send verification email + firebaseAuth.currentUser!!.sendEmailVerification().await() + + // TODO: Show snackbar "Verification email sent to ${user.email} " + } catch (e: Exception) { + Log.e(TAG, "sendEmailVerification", e) + // TODO: Show snackbar "Failed to send verification email." + } finally { + // Re-enable button + _uiState.update { it.copy(isVerifyEmailVisible = true) } + } + } + } + + fun reload() { + viewModelScope.launch { + try { + firebaseAuth.currentUser!!.reload().await() + updateUiState(firebaseAuth.currentUser) + // TODO(thatfiredev): Show "Reload successful" snackbar + } catch (e: Exception) { + Log.e(TAG, "reload", e) + // TODO(thatfiredev): Show "Failed to reload user." snackbar + } + } + } + + 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, + isCreateAccountEnabled = false, + isProgressBarVisible = false + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + userId = "Firebase UID: ${user.uid}", + userEmail = "Email User: ${user.email} (verified: ${user.isEmailVerified})", + isSignInEnabled = false, + isVerifyEmailVisible = !user.isEmailVerified, + isCreateAccountEnabled = true, + isProgressBarVisible = false + ) + } + } + } + + private fun toggleProgressbar(isVisible: Boolean) { + _uiState.update { it.copy(isProgressBarVisible = isVisible) } + } + + 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 + } + + companion object { + private const val TAG = "EmailPasswordViewModel" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginFragment.kt index 25147e24a..25c93907c 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginFragment.kt @@ -5,20 +5,20 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import com.facebook.AccessToken +import androidx.core.view.isGone +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.facebook.CallbackManager import com.facebook.FacebookCallback import com.facebook.FacebookException -import com.facebook.login.LoginManager import com.facebook.login.LoginResult -import com.google.firebase.auth.FacebookAuthProvider 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 com.google.firebase.quickstart.auth.databinding.FragmentFacebookBinding +import kotlinx.coroutines.launch /** * Demonstrate Firebase Authentication using a Facebook access token. @@ -31,7 +31,7 @@ class FacebookLoginFragment : BaseFragment() { private val binding: FragmentFacebookBinding get() = _binding!! - private lateinit var callbackManager: CallbackManager + private val viewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentFacebookBinding.inflate(layoutInflater, container, false) @@ -42,85 +42,48 @@ class FacebookLoginFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) setProgressBar(binding.progressBar) - binding.buttonFacebookSignout.setOnClickListener { signOut() } + binding.buttonFacebookSignout.setOnClickListener { viewModel.signOut() } // Initialize Firebase Auth auth = Firebase.auth // Initialize Facebook Login button - callbackManager = CallbackManager.Factory.create() + val callbackManager = CallbackManager.Factory.create() binding.buttonFacebookLogin.setPermissions("email", "public_profile") binding.buttonFacebookLogin.registerCallback(callbackManager, object : FacebookCallback { - override fun onSuccess(loginResult: LoginResult) { - Log.d(TAG, "facebook:onSuccess:$loginResult") - handleFacebookAccessToken(loginResult.accessToken) + override fun onSuccess(result: LoginResult) { + Log.d(TAG, "facebook:onSuccess:$result") + viewModel.handleFacebookAccessToken(result.accessToken) } override fun onCancel() { Log.d(TAG, "facebook:onCancel") - updateUI(null) + viewModel.showInitialState() } override fun onError(error: FacebookException) { Log.d(TAG, "facebook:onError", error) - updateUI(null) + viewModel.showInitialState() } }) - } - override fun onStart() { - super.onStart() - // Check if user is signed in (non-null) and update UI accordingly. - val currentUser = auth.currentUser - updateUI(currentUser) - } + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + binding.status.text = uiState.status + binding.detail.text = uiState.detail + + binding.buttonFacebookLogin.isGone = !uiState.isSignInVisible + binding.buttonFacebookSignout.isGone = uiState.isSignInVisible - private fun handleFacebookAccessToken(token: AccessToken) { - Log.d(TAG, "handleFacebookAccessToken:$token") - showProgressBar() - - val credential = FacebookAuthProvider.getCredential(token.token) - auth.signInWithCredential(credential) - .addOnCompleteListener(requireActivity()) { task -> - if (task.isSuccessful) { - // Sign in success, update UI with the signed-in user's information - Log.d(TAG, "signInWithCredential:success") - val user = auth.currentUser - updateUI(user) + if (uiState.isProgressBarVisible) { + showProgressBar() } else { - // If sign in fails, display a message to the user. - Log.w(TAG, "signInWithCredential:failure", task.exception) - Toast.makeText(context, "Authentication failed.", - Toast.LENGTH_SHORT).show() - updateUI(null) + hideProgressBar() } - - hideProgressBar() } - } - - fun signOut() { - auth.signOut() - LoginManager.getInstance().logOut() - - updateUI(null) - } - - private fun updateUI(user: FirebaseUser?) { - hideProgressBar() - if (user != null) { - binding.status.text = getString(R.string.facebook_status_fmt, user.displayName) - binding.detail.text = getString(R.string.firebase_status_fmt, user.uid) - - binding.buttonFacebookLogin.visibility = View.GONE - binding.buttonFacebookSignout.visibility = View.VISIBLE - } else { - binding.status.setText(R.string.signed_out) - binding.detail.text = null - - binding.buttonFacebookLogin.visibility = View.VISIBLE - binding.buttonFacebookSignout.visibility = View.GONE + } } } @@ -130,6 +93,6 @@ class FacebookLoginFragment : BaseFragment() { } companion object { - private const val TAG = "FacebookLogin" + private const val TAG = "FacebookLoginFragment" } } diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginViewModel.kt new file mode 100644 index 000000000..b52ae9c34 --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginViewModel.kt @@ -0,0 +1,99 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.facebook.AccessToken +import com.facebook.login.LoginManager +import com.google.firebase.auth.FacebookAuthProvider +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 FacebookLoginViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + data class UiState( + var status: String = "", + var detail: String? = null, + var isSignInVisible: Boolean = true, + var isProgressBarVisible: Boolean = false + ) + + init { + // Check if user is signed in (non-null) and update UI accordingly. + val firebaseUser = firebaseAuth.currentUser + updateUiState(firebaseUser) + } + + fun handleFacebookAccessToken(token: AccessToken) { + Log.d(TAG, "handleFacebookAccessToken:$token") + toggleProgressbar(isVisible = true) + + val credential = FacebookAuthProvider.getCredential(token.token) + viewModelScope.launch { + try { + val authResult = firebaseAuth.signInWithCredential(credential).await() + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "signInWithCredential:success") + updateUiState(authResult.user) + } catch (e: Exception) { + // If sign in fails, display a message to the user. + Log.w(TAG, "signInWithCredential:failure", e) + // TODO(thatfiredev): Show snackbar "Authentication failed." + updateUiState(null) + } finally { + toggleProgressbar(isVisible = false) + } + } + } + + fun showInitialState() { + updateUiState(null) + } + + fun signOut() { + firebaseAuth.signOut() + LoginManager.getInstance().logOut() + updateUiState(null) + } + + private fun updateUiState(user: FirebaseUser?) { + if (user != null) { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Facebook User: ${user.displayName}", + detail = "Firebase UID: ${user.uid}", + isSignInVisible = false, + isProgressBarVisible = false + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Signed out", + detail = null, + isSignInVisible = true, + isProgressBarVisible = false + ) + } + } + } + + private fun toggleProgressbar(isVisible: Boolean) { + _uiState.update { it.copy(isProgressBarVisible = isVisible) } + } + + companion object { + const val TAG = "FacebookLoginViewModel" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIFragment.kt index 86ec069c6..56bea5721 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIFragment.kt @@ -5,18 +5,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast +import androidx.core.view.isGone import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.firebase.ui.auth.AuthUI import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult -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.android.material.snackbar.Snackbar import com.google.firebase.quickstart.auth.BuildConfig import com.google.firebase.quickstart.auth.R import com.google.firebase.quickstart.auth.databinding.FragmentFirebaseUiBinding +import kotlinx.coroutines.launch /** * Demonstrate authentication using the FirebaseUI-Android library. This fragment demonstrates @@ -26,7 +28,7 @@ import com.google.firebase.quickstart.auth.databinding.FragmentFirebaseUiBinding */ class FirebaseUIFragment : Fragment() { - private lateinit var auth: FirebaseAuth + private val viewModel by viewModels() private var _binding: FragmentFirebaseUiBinding? = null private val binding: FragmentFirebaseUiBinding @@ -36,7 +38,7 @@ class FirebaseUIFragment : Fragment() { // possible customization see: https://github.com/firebase/firebaseui-android private val signInLauncher = registerForActivityResult( FirebaseAuthUIActivityResultContract() - ) { result -> this.onSignInResult(result)} + ) { result -> this.onSignInResult(result) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentFirebaseUiBinding.inflate(inflater, container, false) @@ -46,62 +48,42 @@ class FirebaseUIFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Initialize Firebase Auth - auth = Firebase.auth - binding.signInButton.setOnClickListener { startSignIn() } - binding.signOutButton.setOnClickListener { signOut() } - } - - override fun onStart() { - super.onStart() - updateUI(auth.currentUser) + binding.signOutButton.setOnClickListener { viewModel.signOut() } + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + binding.status.text = uiState.status + binding.detail.text = uiState.detail + + binding.signInButton.isGone = !uiState.isSignInVisible + binding.signOutButton.isGone = uiState.isSignInVisible + } + } + } } private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) { if (result.resultCode == Activity.RESULT_OK) { // Sign in succeeded - updateUI(auth.currentUser) } else { // Sign in failed - Toast.makeText(context, "Sign In Failed", Toast.LENGTH_SHORT).show() - updateUI(null) + Snackbar.make(requireView(), "Sign In Failed", Snackbar.LENGTH_SHORT).show() } + viewModel.showSignedInUser() } private fun startSignIn() { val intent = AuthUI.getInstance().createSignInIntentBuilder() - .setIsSmartLockEnabled(!BuildConfig.DEBUG) - .setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build())) - .setLogo(R.mipmap.ic_launcher) - .build() + .setIsSmartLockEnabled(!BuildConfig.DEBUG) + .setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build())) + .setLogo(R.mipmap.ic_launcher) + .build() signInLauncher.launch(intent) } - private fun updateUI(user: FirebaseUser?) { - if (user != null) { - // Signed in - binding.status.text = getString(R.string.firebaseui_status_fmt, user.email) - binding.detail.text = getString(R.string.id_fmt, user.uid) - - binding.signInButton.visibility = View.GONE - binding.signOutButton.visibility = View.VISIBLE - } else { - // Signed out - binding.status.setText(R.string.signed_out) - binding.detail.text = null - - binding.signInButton.visibility = View.VISIBLE - binding.signOutButton.visibility = View.GONE - } - } - - private fun signOut() { - AuthUI.getInstance().signOut(requireContext()) - updateUI(null) - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIViewModel.kt new file mode 100644 index 000000000..95ef70b11 --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIViewModel.kt @@ -0,0 +1,61 @@ +package com.google.firebase.quickstart.auth.kotlin + +import androidx.lifecycle.ViewModel +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 + +class FirebaseUIViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + data class UiState( + var status: String = "", + var detail: String? = null, + var isSignInVisible: Boolean = true + ) + + init { + // Check if user is signed in (non-null) and update UI accordingly. + showSignedInUser() + } + + fun showSignedInUser() { + val firebaseUser = firebaseAuth.currentUser + updateUiState(firebaseUser) + } + + fun signOut() { + updateUiState(null) + } + + private fun updateUiState(user: FirebaseUser?) { + if (user != null) { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Firebase User: ${user.displayName}", + detail = "Firebase UID: ${user.uid}", + isSignInVisible = false + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Signed out", + detail = null, + isSignInVisible = true + ) + } + } + } + + companion object { + const val TAG = "FirebaseUIViewModel" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpFragment.kt index 3ce5f3b78..9e77c856e 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpFragment.kt @@ -23,22 +23,29 @@ import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter -import android.widget.Toast -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseUser +import androidx.core.view.isGone +import androidx.fragment.app.viewModels +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.snackbar.Snackbar import com.google.firebase.auth.ktx.auth import com.google.firebase.auth.ktx.oAuthProvider import com.google.firebase.ktx.Firebase import com.google.firebase.quickstart.auth.R import com.google.firebase.quickstart.auth.databinding.FragmentGenericIdpBinding -import java.util.ArrayList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await /** * Demonstrate Firebase Authentication using a Generic Identity Provider (IDP). */ class GenericIdpFragment : BaseFragment() { - private lateinit var auth: FirebaseAuth + private val viewModel by viewModels() private var _binding: FragmentGenericIdpBinding? = null private val binding: FragmentGenericIdpBinding @@ -53,24 +60,49 @@ class GenericIdpFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Initialize Firebase Auth - auth = Firebase.auth // Set up button click listeners - binding.genericSignInButton.setOnClickListener { signIn() } + binding.genericSignInButton.setOnClickListener { + val providerName = spinnerAdapter.getItem(binding.providerSpinner.selectedItemPosition) + if (providerName != null) { + signIn(providerName) + } else { + Snackbar.make(requireView(), "No provider selected", Snackbar.LENGTH_SHORT).show() + } + } binding.signOutButton.setOnClickListener { - auth.signOut() - updateUI(null) + viewModel.signOut() + } + + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + viewModel.showSignedInUser() + } + }) + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + binding.status.text = uiState.status + binding.detail.text = uiState.detail + + binding.genericSignInButton.isGone = !uiState.isSignInVisible + binding.signOutButton.isGone = uiState.isSignInVisible + binding.spinnerLayout.isGone = !uiState.isSignInVisible + + + } + } } // Spinner - val providers = ArrayList(PROVIDER_MAP.keys) - spinnerAdapter = ArrayAdapter(requireContext(), R.layout.item_spinner_list, providers) + spinnerAdapter = ArrayAdapter(requireContext(), R.layout.item_spinner_list, ArrayList(PROVIDER_MAP.keys)) binding.providerSpinner.adapter = spinnerAdapter binding.providerSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { binding.genericSignInButton.text = - getString(R.string.generic_signin_fmt, spinnerAdapter.getItem(position)) + getString(R.string.generic_signin_fmt, spinnerAdapter.getItem(position)) } override fun onNothingSelected(parent: AdapterView<*>) {} @@ -78,73 +110,30 @@ class GenericIdpFragment : BaseFragment() { binding.providerSpinner.setSelection(0) } - override fun onStart() { - super.onStart() - // Check if user is signed in (non-null) and update UI accordingly. - val currentUser = auth.currentUser - updateUI(currentUser) - - // Look for a pending auth result - val pending = auth.pendingAuthResult - if (pending != null) { - pending.addOnSuccessListener { authResult -> - Log.d(TAG, "checkPending:onSuccess:$authResult") - updateUI(authResult.user) - }.addOnFailureListener { e -> - Log.w(TAG, "checkPending:onFailure", e) - } - } else { - Log.d(TAG, "checkPending: null") - } - } - - private fun signIn() { + private fun signIn(providerName: String) { // Could add custom scopes here - val customScopes = ArrayList() + val customScopes = listOf() // Examples of provider ID: apple.com (Apple), microsoft.com (Microsoft), yahoo.com (Yahoo) - val providerId = getProviderId() - - auth.startActivityForSignInWithProvider(requireActivity(), - oAuthProvider(providerId, auth) { - scopes = customScopes - }) - .addOnSuccessListener { authResult -> - Log.d(TAG, "activitySignIn:onSuccess:${authResult.user}") - updateUI(authResult.user) - } - .addOnFailureListener { e -> - Log.w(TAG, "activitySignIn:onFailure", e) - showToast(getString(R.string.error_sign_in_failed)) - } - } + val providerId = PROVIDER_MAP[providerName]!! - private fun getProviderId(): String { - val providerName = spinnerAdapter.getItem(binding.providerSpinner.selectedItemPosition) - return PROVIDER_MAP[providerName!!] ?: error("No provider selected") - } - - private fun updateUI(user: FirebaseUser?) { - hideProgressBar() - if (user != null) { - binding.status.text = getString(R.string.generic_status_fmt, user.displayName, user.email) - binding.detail.text = getString(R.string.firebase_status_fmt, user.uid) - - binding.spinnerLayout.visibility = View.GONE - binding.genericSignInButton.visibility = View.GONE - binding.signOutButton.visibility = View.VISIBLE - } else { - binding.status.setText(R.string.signed_out) - binding.detail.text = null - - binding.spinnerLayout.visibility = View.VISIBLE - binding.genericSignInButton.visibility = View.VISIBLE - binding.signOutButton.visibility = View.GONE + val oAuthProvider = oAuthProvider(providerId) { + scopes = customScopes } - } - private fun showToast(message: String) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + lifecycleScope.launch(Dispatchers.IO) { + try { + val authResult = + Firebase.auth.startActivityForSignInWithProvider(requireActivity(), oAuthProvider).await() + authResult.user?.let { + Log.d("GenericIdpFragment", "activitySignIn:onSuccess:${authResult.user}") + viewModel.showSignedInUser(it) + } + } catch (e: Exception) { + Log.w("GenericIdpFragment", "activitySignIn:onFailure", e) + // TODO(thatfiredev): Snackbar Sign in failed, see logs for details. + } + } } override fun onDestroyView() { @@ -153,12 +142,12 @@ class GenericIdpFragment : BaseFragment() { } companion object { - private const val TAG = "GenericIdp" + const val TAG = "GenericIdpFragment" private val PROVIDER_MAP = mapOf( - "Apple" to "apple.com", - "Microsoft" to "microsoft.com", - "Yahoo" to "yahoo.com", - "Twitter" to "twitter.com" + "Apple" to "apple.com", + "Microsoft" to "microsoft.com", + "Yahoo" to "yahoo.com", + "Twitter" to "twitter.com" ) } } diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpViewModel.kt new file mode 100644 index 000000000..008654c9e --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpViewModel.kt @@ -0,0 +1,82 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 GenericIdpViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + data class UiState( + var status: String = "", + var detail: String? = null, + var isSignInVisible: Boolean = true + ) + + fun showSignedInUser() { + // Check if user is signed in (non-null) and update UI accordingly. + val firebaseUser = firebaseAuth.currentUser + updateUiState(firebaseUser) + + // Look for a pending auth result + val pendingAuthResult = firebaseAuth.pendingAuthResult + if (pendingAuthResult != null) { + viewModelScope.launch { + try { + val authResult = pendingAuthResult.await() + Log.d(TAG, "checkPending:onSuccess:$authResult") + updateUiState(authResult.user) + } catch (e: Exception) { + Log.w(TAG, "checkPending:onFailure", e) + } + } + } else { + Log.d(TAG, "checkPending: null") + } + } + + fun showSignedInUser(user: FirebaseUser) { + updateUiState(user) + } + + fun signOut() { + firebaseAuth.signOut() + updateUiState(null) + } + + private fun updateUiState(user: FirebaseUser?) { + if (user != null) { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "User: ${user.displayName} ${user.email}", + detail = "Firebase UID: ${user.uid}", + isSignInVisible = false + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Signed out", + detail = null, + isSignInVisible = true + ) + } + } + } + + companion object { + const val TAG = "GenericIdpViewModel" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInFragment.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInFragment.kt index 95be1b46a..4ef809dbc 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInFragment.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInFragment.kt @@ -11,6 +11,11 @@ import android.view.View import android.view.ViewGroup import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult +import androidx.core.view.isGone +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.GetSignInIntentRequest import com.google.android.gms.auth.api.identity.Identity @@ -23,19 +28,19 @@ import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase import com.google.firebase.quickstart.auth.R import com.google.firebase.quickstart.auth.databinding.FragmentGoogleBinding +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await /** * Demonstrate Firebase Authentication using a Google ID Token. */ class GoogleSignInFragment : BaseFragment() { - - private lateinit var auth: FirebaseAuth - private var _binding: FragmentGoogleBinding? = null private val binding: FragmentGoogleBinding get() = _binding!! private lateinit var signInClient: SignInClient + private val viewModel by viewModels() private val signInLauncher = registerForActivityResult(StartIntentSenderForResult()) { result -> handleSignInResult(result.data) @@ -58,21 +63,28 @@ class GoogleSignInFragment : BaseFragment() { // Configure Google Sign In signInClient = Identity.getSignInClient(requireContext()) - // Initialize Firebase Auth - auth = Firebase.auth + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + binding.status.text = uiState.status + binding.detail.text = uiState.detail - // Display One-Tap Sign In if user isn't logged in - val currentUser = auth.currentUser - if (currentUser == null) { - oneTapSignIn() - } - } + binding.signInButton.isGone = !uiState.isSignInVisible + binding.signOutButton.isGone = uiState.isSignInVisible + + if (uiState.isProgressBarVisible) { + showProgressBar() + } else { + hideProgressBar() + } - override fun onStart() { - super.onStart() - // Check if user is signed in (non-null) and update UI accordingly. - val currentUser = auth.currentUser - updateUI(currentUser) + // Display One-Tap Sign In if user isn't logged in + if (uiState.isOneTapUiShown) { + oneTapSignIn() + } + } + } + } } private fun handleSignInResult(data: Intent?) { @@ -83,7 +95,7 @@ class GoogleSignInFragment : BaseFragment() { val idToken = credential.googleIdToken if (idToken != null) { Log.d(TAG, "firebaseAuthWithGoogle: ${credential.id}") - firebaseAuthWithGoogle(idToken) + viewModel.signInWithFirebase(idToken) } else { // Shouldn't happen. Log.d(TAG, "No ID token!") @@ -91,44 +103,23 @@ class GoogleSignInFragment : BaseFragment() { } catch (e: ApiException) { // Google Sign In failed, update UI appropriately Log.w(TAG, "Google sign in failed", e) - updateUI(null) + viewModel.showInitialState() } } - private fun firebaseAuthWithGoogle(idToken: String) { - showProgressBar() - val credential = GoogleAuthProvider.getCredential(idToken, null) - auth.signInWithCredential(credential) - .addOnCompleteListener(requireActivity()) { task -> - if (task.isSuccessful) { - // Sign in success, update UI with the signed-in user's information - Log.d(TAG, "signInWithCredential:success") - val user = auth.currentUser - updateUI(user) - } else { - // If sign in fails, display a message to the user. - Log.w(TAG, "signInWithCredential:failure", task.exception) - val view = binding.mainLayout - Snackbar.make(view, "Authentication Failed.", Snackbar.LENGTH_SHORT).show() - updateUI(null) - } - - hideProgressBar() - } - } - private fun signIn() { val signInRequest = GetSignInIntentRequest.builder() .setServerClientId(getString(R.string.default_web_client_id)) .build() - signInClient.getSignInIntent(signInRequest) - .addOnSuccessListener { pendingIntent -> + lifecycleScope.launch { + try { + val pendingIntent = signInClient.getSignInIntent(signInRequest).await() launchSignIn(pendingIntent) - } - .addOnFailureListener { e -> + } catch (e: Exception) { Log.e(TAG, "Google Sign-in failed", e) } + } } private fun oneTapSignIn() { @@ -144,14 +135,15 @@ class GoogleSignInFragment : BaseFragment() { .build() // Display the One Tap UI - signInClient.beginSignIn(oneTapRequest) - .addOnSuccessListener { result -> - launchSignIn(result.pendingIntent) - } - .addOnFailureListener { e -> + lifecycleScope.launch { + try { + val beginSignInResult = signInClient.beginSignIn(oneTapRequest).await() + launchSignIn(beginSignInResult.pendingIntent) + } catch (e: Exception) { // No saved credentials found. Launch the One Tap sign-up flow, or // do nothing and continue presenting the signed-out UI. } + } } private fun launchSignIn(pendingIntent: PendingIntent) { @@ -166,28 +158,17 @@ class GoogleSignInFragment : BaseFragment() { private fun signOut() { // Firebase sign out - auth.signOut() + viewModel.signOut() // Google sign out - signInClient.signOut().addOnCompleteListener(requireActivity()) { - updateUI(null) - } - } - - private fun updateUI(user: FirebaseUser?) { - hideProgressBar() - if (user != null) { - binding.status.text = getString(R.string.google_status_fmt, user.email) - binding.detail.text = getString(R.string.firebase_status_fmt, user.uid) - - binding.signInButton.visibility = View.GONE - binding.signOutButton.visibility = View.VISIBLE - } else { - binding.status.setText(R.string.signed_out) - binding.detail.text = null - - binding.signInButton.visibility = View.VISIBLE - binding.signOutButton.visibility = View.GONE + lifecycleScope.launch { + try { + signInClient.signOut().await() + } catch (e: Exception) { + // Google Sign out failed + } finally { + viewModel.showInitialState() + } } } diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInViewModel.kt new file mode 100644 index 000000000..6c539bde0 --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInViewModel.kt @@ -0,0 +1,97 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.auth.FirebaseAuth +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +class GoogleSignInViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + data class UiState( + var status: String = "", + var detail: String? = null, + var isSignInVisible: Boolean = true, + var isProgressBarVisible: Boolean = false, + var isOneTapUiShown: Boolean = false + ) + + init { + // Check if user is signed in (non-null) and update UI accordingly. + val firebaseUser = firebaseAuth.currentUser + updateUiState(firebaseUser) + if (firebaseUser == null) { + _uiState.update { it.copy(isOneTapUiShown = true) } + } + } + + fun showInitialState() { + updateUiState(null) + } + + fun signInWithFirebase(googleIdToken: String) { + val credential = GoogleAuthProvider.getCredential(googleIdToken, null) + viewModelScope.launch { + toggleProgressbar(isVisible = true) + try { + val authResult = firebaseAuth.signInWithCredential(credential).await() + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "signInWithCredential:success") + updateUiState(authResult.user) + } catch (e: Exception) { + // If sign in fails, display a message to the user. + Log.w(TAG, "signInWithCredential:failure", e) + // TODO(thatfiredev) "Authentication Failed." + updateUiState(null) + } finally { + toggleProgressbar(isVisible = false) + } + } + } + + fun signOut() { + firebaseAuth.signOut() + } + + private fun updateUiState(user: FirebaseUser?) { + if (user != null) { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Google User: ${user.displayName}", + detail = "Firebase UID: ${user.uid}", + isSignInVisible = false, + isProgressBarVisible = false + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Signed out", + detail = null, + isSignInVisible = true, + isProgressBarVisible = false + ) + } + } + } + + private fun toggleProgressbar(isVisible: Boolean) { + _uiState.update { it.copy(isProgressBarVisible = isVisible) } + } + + companion object { + const val TAG = "GoogleSignInViewModel" + } +} \ No newline at end of file diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessActivity.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessActivity.kt index f42122bcb..6e0401192 100644 --- a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessActivity.kt +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessActivity.kt @@ -2,43 +2,56 @@ package com.google.firebase.quickstart.auth.kotlin import android.content.Intent import android.os.Bundle -import com.google.android.material.snackbar.Snackbar -import android.text.TextUtils -import android.util.Log -import android.view.View -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseAuthActionCodeException -import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException -import com.google.firebase.auth.FirebaseUser -import com.google.firebase.auth.ktx.actionCodeSettings -import com.google.firebase.auth.ktx.auth -import com.google.firebase.ktx.Firebase +import androidx.activity.viewModels +import androidx.core.view.isGone +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.firebase.quickstart.auth.R import com.google.firebase.quickstart.auth.databinding.ActivityPasswordlessBinding +import kotlinx.coroutines.launch /** * Demonstrate Firebase Authentication without a password, using a link sent to an * email address. */ -class PasswordlessActivity : BaseActivity(), View.OnClickListener { +class PasswordlessActivity : BaseActivity() { private var pendingEmail: String = "" - private var emailLink: String = "" - private lateinit var auth: FirebaseAuth private lateinit var binding: ActivityPasswordlessBinding + private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityPasswordlessBinding.inflate(layoutInflater) setContentView(binding.root) setProgressBar(binding.progressBar) - // Initialize Firebase Auth - auth = Firebase.auth + binding.passwordlessSendEmailButton.setOnClickListener { + val email = binding.fieldEmail.text.toString() + if (email.isBlank()) { + binding.fieldEmail.error = "Email must not be empty." + return@setOnClickListener + } - binding.passwordlessSendEmailButton.setOnClickListener(this) - binding.passwordlessSignInButton.setOnClickListener(this) - binding.signOutButton.setOnClickListener(this) + hideKeyboard(binding.fieldEmail) + viewModel.sendSignInLink(email, packageName) + } + binding.passwordlessSignInButton.setOnClickListener { + val email = binding.fieldEmail.text.toString() + if (email.isBlank()) { + binding.fieldEmail.error = "Email must not be empty." + return@setOnClickListener + } + + hideKeyboard(binding.fieldEmail) + viewModel.signInWithEmailLink(email) + } + binding.signOutButton.setOnClickListener { + viewModel.signOut() + binding.status.setText(R.string.status_email_not_sent) + } // Restore the "pending" email address if (savedInstanceState != null) { @@ -48,11 +61,16 @@ class PasswordlessActivity : BaseActivity(), View.OnClickListener { // Check if the Intent that started the Activity contains an email sign-in link. checkIntent(intent) - } - override fun onStart() { - super.onStart() - updateUI(auth.currentUser) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + binding.status.text = uiState.status + binding.passwordlessButtons.isGone = !uiState.isSignInEnabled + binding.signOutButton.isGone = uiState.isSignInEnabled + } + } + } } override fun onNewIntent(intent: Intent) { @@ -72,8 +90,6 @@ class PasswordlessActivity : BaseActivity(), View.OnClickListener { */ private fun checkIntent(intent: Intent?) { if (intentHasEmailLink(intent)) { - emailLink = intent!!.data!!.toString() - binding.status.setText(R.string.status_link_found) binding.passwordlessSendEmailButton.isEnabled = false binding.passwordlessSignInButton.isEnabled = true @@ -90,133 +106,14 @@ class PasswordlessActivity : BaseActivity(), View.OnClickListener { private fun intentHasEmailLink(intent: Intent?): Boolean { if (intent != null && intent.data != null) { val intentData = intent.data.toString() - if (auth.isSignInWithEmailLink(intentData)) { + if (viewModel.isSignInWithEmailLink(intentData)) { return true } } - return false } - /** - * Send an email sign-in link to the specified email. - */ - private fun sendSignInLink(email: String) { - val settings = actionCodeSettings { - setAndroidPackageName( - packageName, - false, null/* minimum app version */)/* install if not available? */ - handleCodeInApp = true - url = "https://kotlin.auth.example.com/emailSignInLink" - } - - hideKeyboard(binding.fieldEmail) - showProgressBar() - - auth.sendSignInLinkToEmail(email, settings) - .addOnCompleteListener { task -> - hideProgressBar() - - if (task.isSuccessful) { - Log.d(TAG, "Link sent") - showSnackbar("Sign-in link sent!") - - pendingEmail = email - binding.status.setText(R.string.status_email_sent) - } else { - val e = task.exception - Log.w(TAG, "Could not send link", e) - showSnackbar("Failed to send link.") - - if (e is FirebaseAuthInvalidCredentialsException) { - binding.fieldEmail.error = "Invalid email address." - } - } - } - } - - /** - * Sign in using an email address and a link, the link is passed to the Activity - * from the dynamic link contained in the email. - */ - private fun signInWithEmailLink(email: String, link: String?) { - Log.d(TAG, "signInWithLink:" + link!!) - - hideKeyboard(binding.fieldEmail) - showProgressBar() - - auth.signInWithEmailLink(email, link) - .addOnCompleteListener { task -> - hideProgressBar() - if (task.isSuccessful) { - Log.d(TAG, "signInWithEmailLink:success") - - binding.fieldEmail.text = null - updateUI(task.result?.user) - } else { - Log.w(TAG, "signInWithEmailLink:failure", task.exception) - updateUI(null) - - if (task.exception is FirebaseAuthActionCodeException) { - showSnackbar("Invalid or expired sign-in link.") - } - } - } - } - - private fun onSendLinkClicked() { - val email = binding.fieldEmail.text.toString() - if (TextUtils.isEmpty(email)) { - binding.fieldEmail.error = "Email must not be empty." - return - } - - sendSignInLink(email) - } - - private fun onSignInClicked() { - val email = binding.fieldEmail.text.toString() - if (TextUtils.isEmpty(email)) { - binding.fieldEmail.error = "Email must not be empty." - return - } - - signInWithEmailLink(email, emailLink) - } - - private fun onSignOutClicked() { - auth.signOut() - - updateUI(null) - binding.status.setText(R.string.status_email_not_sent) - } - - private fun updateUI(user: FirebaseUser?) { - if (user != null) { - binding.status.text = getString(R.string.passwordless_status_fmt, - user.email, user.isEmailVerified) - binding.passwordlessButtons.visibility = View.GONE - binding.signOutButton.visibility = View.VISIBLE - } else { - binding.passwordlessButtons.visibility = View.VISIBLE - binding.signOutButton.visibility = View.GONE - } - } - - private fun showSnackbar(message: String) { - Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_SHORT).show() - } - - override fun onClick(view: View) { - when (view.id) { - R.id.passwordlessSendEmailButton -> onSendLinkClicked() - R.id.passwordlessSignInButton -> onSignInClicked() - R.id.signOutButton -> onSignOutClicked() - } - } - companion object { - private const val TAG = "PasswordlessSignIn" private const val KEY_PENDING_EMAIL = "key_pending_email" } } diff --git a/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessViewModel.kt b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessViewModel.kt new file mode 100644 index 000000000..bac8c8e53 --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessViewModel.kt @@ -0,0 +1,137 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthActionCodeException +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.auth.ktx.actionCodeSettings +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 PasswordlessViewModel( + private val firebaseAuth: FirebaseAuth = Firebase.auth +) : ViewModel() { + private var pendingEmail: String = "" + private var emailLink: String = "" + + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState + + data class UiState( + var status: String? = null, + var userEmail: String? = null, + var isSignInEnabled: Boolean = true, + var isProgressBarVisible: Boolean = false, + var emailError: String? = null + ) + + init { + val firebaseUser = firebaseAuth.currentUser + updateUiState(firebaseUser) + } + + fun sendSignInLink(email: String, appPackageName: String) { + val settings = actionCodeSettings { + setAndroidPackageName( + appPackageName, + false, null/* minimum app version */)/* install if not available? */ + handleCodeInApp = true + url = "https://kotlin.auth.example.com/emailSignInLink" + } + + toggleProgressbar(isVisible = true) + + viewModelScope.launch { + try { + firebaseAuth.sendSignInLinkToEmail(email, settings).await() + Log.d(TAG, "Link sent") + // TODO(thatfiredev): showSnackbar("Sign-in link sent!") + + pendingEmail = email + _uiState.update { it.copy(status = "Link sent, check your email to continue.") } + } catch (e: Exception) { + Log.w(TAG, "Could not send link", e) + // TODO(thatfiredev): showSnackbar("Failed to send link.") + if (e is FirebaseAuthInvalidCredentialsException) { + _uiState.update { it.copy(emailError = "Invalid email address.") } + } + } finally { + toggleProgressbar(isVisible = false) + } + } + } + + fun isSignInWithEmailLink(link: String): Boolean { + if (firebaseAuth.isSignInWithEmailLink(link)) { + emailLink = link + return true + } + return false + } + + fun signInWithEmailLink(email: String) { + toggleProgressbar(isVisible = true) + + viewModelScope.launch { + try { + Log.d(TAG, "signInWithLink:$emailLink") + val authResult = firebaseAuth.signInWithEmailLink(email, emailLink).await() + Log.d(TAG, "signInWithEmailLink:success") + _uiState.update { it.copy(userEmail = null) } + updateUiState(authResult.user) + } catch (e: Exception) { + Log.w(TAG, "signInWithEmailLink:failure", e) + updateUiState(null) + + if (e is FirebaseAuthActionCodeException) { + // TODO(thatfiredev): showSnackbar("Invalid or expired sign-in link.") + } + } finally { + toggleProgressbar(isVisible = false) + } + } + } + + fun signOut() { + firebaseAuth.signOut() + updateUiState(null) + } + + private fun updateUiState(user: FirebaseUser?) { + if (user != null) { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Email User: ${user.email} (verified: ${user.isEmailVerified})", + userEmail = "Email: ${user.email}", + isSignInEnabled = false, + isProgressBarVisible = false + ) + } + } else { + _uiState.update { currentUiState -> + currentUiState.copy( + status = "Signed Out", + userEmail = null, + isSignInEnabled = true, + isProgressBarVisible = false + ) + } + } + } + + private fun toggleProgressbar(isVisible: Boolean) { + _uiState.update { it.copy(isProgressBarVisible = isVisible) } + } + + companion object { + const val TAG = "PasswordlessViewModel" + } +} \ No newline at end of file