From bb650c89d84a5df66db76c25a6aebd6233a332b6 Mon Sep 17 00:00:00 2001 From: rosariopf Date: Thu, 16 Feb 2023 22:56:02 +0000 Subject: [PATCH] refactor(genericidp): move Firebase calls to ViewModel --- .../auth/kotlin/GenericIdpFragment.kt | 151 ++++++------------ .../auth/kotlin/GenericIdpViewModel.kt | 111 +++++++++++++ 2 files changed, 159 insertions(+), 103 deletions(-) create mode 100644 auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpViewModel.kt 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..d31180bfb 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 @@ -17,28 +17,29 @@ package com.google.firebase.quickstart.auth.kotlin import android.os.Bundle -import android.util.Log import android.view.LayoutInflater 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 com.google.firebase.auth.ktx.auth -import com.google.firebase.auth.ktx.oAuthProvider -import com.google.firebase.ktx.Firebase +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.quickstart.auth.R import com.google.firebase.quickstart.auth.databinding.FragmentGenericIdpBinding -import java.util.ArrayList +import kotlinx.coroutines.launch /** * 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,112 +54,56 @@ 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.signOutButton.setOnClickListener { - auth.signOut() - updateUI(null) - } - - // Spinner - val providers = ArrayList(PROVIDER_MAP.keys) - spinnerAdapter = ArrayAdapter(requireContext(), R.layout.item_spinner_list, providers) - 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)) + binding.genericSignInButton.setOnClickListener { + val providerName = spinnerAdapter.getItem(binding.providerSpinner.selectedItemPosition) + if (providerName != null) { + viewModel.signIn(requireActivity(), providerName) + } else { + Snackbar.make(requireView(), "No provider selected", Snackbar.LENGTH_SHORT).show() } - - override fun onNothingSelected(parent: AdapterView<*>) {} } - 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") + binding.signOutButton.setOnClickListener { + viewModel.signOut() } - } - private fun signIn() { - // Could add custom scopes here - val customScopes = ArrayList() - - // 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)) + 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 + spinnerAdapter = ArrayAdapter(requireContext(), R.layout.item_spinner_list, uiState.providerNames) + 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)) + } + + override fun onNothingSelected(parent: AdapterView<*>) {} + } + binding.providerSpinner.setSelection(0) } - } - - 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 + } } } - private fun showToast(message: String) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } - override fun onDestroyView() { super.onDestroyView() _binding = null } - - companion object { - private const val TAG = "GenericIdp" - private val PROVIDER_MAP = mapOf( - "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..00d7acd42 --- /dev/null +++ b/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpViewModel.kt @@ -0,0 +1,111 @@ +package com.google.firebase.quickstart.auth.kotlin + +import android.app.Activity +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.auth.ktx.oAuthProvider +import com.google.firebase.ktx.Firebase +import java.util.ArrayList +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, + val providerNames: List = ArrayList(PROVIDER_MAP.keys) + ) + + 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 signIn(activity: Activity, providerName: String) { + // Could add custom scopes here + val customScopes = listOf() + + // Examples of provider ID: apple.com (Apple), microsoft.com (Microsoft), yahoo.com (Yahoo) + val providerId = PROVIDER_MAP[providerName]!! + + val oAuthProvider = oAuthProvider(providerId) { + scopes = customScopes + } + + viewModelScope.launch { + try { + val authResult = firebaseAuth.startActivityForSignInWithProvider(activity, oAuthProvider).await() + Log.d(TAG, "activitySignIn:onSuccess:${authResult.user}") + updateUiState(authResult.user) + } catch (e: Exception) { + Log.w(TAG, "activitySignIn:onFailure", e) + // TODO(thatfiredev): Snackbar Sign in failed, see logs for details. + } + } + } + + 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" + private val PROVIDER_MAP = mapOf( + "Apple" to "apple.com", + "Microsoft" to "microsoft.com", + "Yahoo" to "yahoo.com", + "Twitter" to "twitter.com" + ) + } +} \ No newline at end of file