Implement logout feature
This commit is contained in:
@@ -2,6 +2,7 @@ package gq.kirmanak.mealie
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -48,4 +49,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
menu.findItem(R.id.logout).isVisible = isAuthenticated
|
menu.findItem(R.id.logout).isVisible = isAuthenticated
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
Timber.v("onOptionsItemSelected() called with: item = $item")
|
||||||
|
val result = if (item.itemId == R.id.logout) {
|
||||||
|
authViewModel.logout()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,4 +10,6 @@ interface AuthRepo {
|
|||||||
suspend fun getToken(): String?
|
suspend fun getToken(): String?
|
||||||
|
|
||||||
fun authenticationStatuses(): Flow<Boolean>
|
fun authenticationStatuses(): Flow<Boolean>
|
||||||
|
|
||||||
|
fun logout()
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,13 @@ package gq.kirmanak.mealie.data.auth
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AuthStorage {
|
interface AuthStorage {
|
||||||
suspend fun storeAuthData(token: String, baseUrl: String)
|
fun storeAuthData(token: String, baseUrl: String)
|
||||||
|
|
||||||
suspend fun getBaseUrl(): String?
|
suspend fun getBaseUrl(): String?
|
||||||
|
|
||||||
suspend fun getToken(): String?
|
suspend fun getToken(): String?
|
||||||
|
|
||||||
fun tokenObservable(): Flow<String?>
|
fun tokenObservable(): Flow<String?>
|
||||||
|
|
||||||
|
fun clearAuthData()
|
||||||
}
|
}
|
||||||
@@ -38,4 +38,9 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
Timber.v("authenticationStatuses() called")
|
Timber.v("authenticationStatuses() called")
|
||||||
return storage.tokenObservable().map { it != null }
|
return storage.tokenObservable().map { it != null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
Timber.v("logout() called")
|
||||||
|
storage.clearAuthData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex
|
|||||||
private val sharedPreferences: SharedPreferences
|
private val sharedPreferences: SharedPreferences
|
||||||
get() = PreferenceManager.getDefaultSharedPreferences(context)
|
get() = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
override suspend fun storeAuthData(token: String, baseUrl: String) {
|
override fun storeAuthData(token: String, baseUrl: String) {
|
||||||
Timber.v("storeAuthData() called with: token = $token, baseUrl = $baseUrl")
|
Timber.v("storeAuthData() called with: token = $token, baseUrl = $baseUrl")
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.putString(TOKEN_KEY, token)
|
.putString(TOKEN_KEY, token)
|
||||||
@@ -47,6 +47,10 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getString(key: String): String? = withContext(Dispatchers.Default) {
|
||||||
|
sharedPreferences.getString(key, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun tokenObservable(): Flow<String?> {
|
override fun tokenObservable(): Flow<String?> {
|
||||||
Timber.v("tokenObservable() called")
|
Timber.v("tokenObservable() called")
|
||||||
return callbackFlow {
|
return callbackFlow {
|
||||||
@@ -70,7 +74,11 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getString(key: String): String? = withContext(Dispatchers.Default) {
|
override fun clearAuthData() {
|
||||||
sharedPreferences.getString(key, null)
|
Timber.v("clearAuthData() called")
|
||||||
|
sharedPreferences.edit()
|
||||||
|
.remove(TOKEN_KEY)
|
||||||
|
.remove(BASE_URL_KEY)
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,9 @@ class AuthenticationViewModel @Inject constructor(
|
|||||||
Timber.v("authenticationStatuses() called")
|
Timber.v("authenticationStatuses() called")
|
||||||
return authRepo.authenticationStatuses()
|
return authRepo.authenticationStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
Timber.v("logout() called")
|
||||||
|
authRepo.logout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,11 @@ import android.view.ViewGroup
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealie.databinding.FragmentRecipesBinding
|
import gq.kirmanak.mealie.databinding.FragmentRecipesBinding
|
||||||
|
import gq.kirmanak.mealie.ui.auth.AuthenticationViewModel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ class RecipesFragment : Fragment() {
|
|||||||
private val binding: FragmentRecipesBinding
|
private val binding: FragmentRecipesBinding
|
||||||
get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" }
|
get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" }
|
||||||
private val viewModel by viewModels<RecipeViewModel>()
|
private val viewModel by viewModels<RecipeViewModel>()
|
||||||
|
private val authViewModel by viewModels<AuthenticationViewModel>()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -33,13 +36,33 @@ class RecipesFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||||
|
setupRecipeAdapter()
|
||||||
|
listenToAuthStatuses()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listenToAuthStatuses() {
|
||||||
|
Timber.v("listenToAuthStatuses() called")
|
||||||
|
lifecycleScope.launchWhenCreated {
|
||||||
|
authViewModel.authenticationStatuses().collectLatest {
|
||||||
|
Timber.v("listenToAuthStatuses: new auth status = $it")
|
||||||
|
if (!it) navigateToAuthFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToAuthFragment() {
|
||||||
|
Timber.v("navigateToAuthFragment() called")
|
||||||
|
findNavController().navigate(RecipesFragmentDirections.actionRecipesFragmentToAuthenticationFragment())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecipeAdapter() {
|
||||||
|
Timber.v("setupRecipeAdapter() called")
|
||||||
binding.recipes.layoutManager = LinearLayoutManager(requireContext())
|
binding.recipes.layoutManager = LinearLayoutManager(requireContext())
|
||||||
val recipesPagingAdapter = RecipesPagingAdapter(viewModel)
|
val recipesPagingAdapter = RecipesPagingAdapter(viewModel)
|
||||||
binding.recipes.adapter = recipesPagingAdapter
|
binding.recipes.adapter = recipesPagingAdapter
|
||||||
lifecycleScope.launchWhenResumed {
|
lifecycleScope.launchWhenResumed {
|
||||||
Timber.d("onViewCreated: coroutine started")
|
|
||||||
viewModel.recipeFlow.collectLatest {
|
viewModel.recipeFlow.collectLatest {
|
||||||
Timber.d("onViewCreated: received update")
|
Timber.d("setupRecipeAdapter: received update")
|
||||||
recipesPagingAdapter.submitData(it)
|
recipesPagingAdapter.submitData(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,11 @@
|
|||||||
android:id="@+id/recipesFragment"
|
android:id="@+id/recipesFragment"
|
||||||
android:name="gq.kirmanak.mealie.ui.recipes.RecipesFragment"
|
android:name="gq.kirmanak.mealie.ui.recipes.RecipesFragment"
|
||||||
android:label="fragment_recipes"
|
android:label="fragment_recipes"
|
||||||
tools:layout="@layout/fragment_recipes" />
|
tools:layout="@layout/fragment_recipes">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_recipesFragment_to_authenticationFragment"
|
||||||
|
app:destination="@id/authenticationFragment"
|
||||||
|
app:popUpTo="@id/nav_graph"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
</fragment>
|
||||||
</navigation>
|
</navigation>
|
||||||
@@ -53,4 +53,25 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
|||||||
subject.storeAuthData(TEST_TOKEN, TEST_URL)
|
subject.storeAuthData(TEST_TOKEN, TEST_URL)
|
||||||
assertThat(subject.tokenObservable().first()).isEqualTo(TEST_TOKEN)
|
assertThat(subject.tokenObservable().first()).isEqualTo(TEST_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when clearAuthData then first token is null`() = runBlocking {
|
||||||
|
subject.storeAuthData(TEST_TOKEN, TEST_URL)
|
||||||
|
subject.clearAuthData()
|
||||||
|
assertThat(subject.tokenObservable().first()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when clearAuthData then getToken returns null`() = runBlocking {
|
||||||
|
subject.storeAuthData(TEST_TOKEN, TEST_URL)
|
||||||
|
subject.clearAuthData()
|
||||||
|
assertThat(subject.getToken()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when clearAuthData then getBaseUrl returns null`() = runBlocking {
|
||||||
|
subject.storeAuthData(TEST_TOKEN, TEST_URL)
|
||||||
|
subject.clearAuthData()
|
||||||
|
assertThat(subject.getBaseUrl()).isNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user