Skip to content

Commit

Permalink
refactor(genericidp): move Firebase calls to ViewModel
Browse files Browse the repository at this point in the history
  • Loading branch information
thatfiredev committed Feb 16, 2023
1 parent bc9912a commit bb650c8
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenericIdpViewModel>()

private var _binding: FragmentGenericIdpBinding? = null
private val binding: FragmentGenericIdpBinding
Expand All @@ -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<String>()

// 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"
)
}
}
Original file line number Diff line number Diff line change
@@ -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> = _uiState

data class UiState(
var status: String = "",
var detail: String? = null,
var isSignInVisible: Boolean = true,
val providerNames: List<String> = 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<String>()

// 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"
)
}
}

0 comments on commit bb650c8

Please sign in to comment.