From a99aafe6d8103b9e1808968227609c0c14fc5ed9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 17 Aug 2022 12:15:26 -0700 Subject: [PATCH] show snackbar when login submission returns an error --- .../omnivore/omnivore/DatastoreRepository.kt | 8 +-- .../app/omnivore/omnivore/LoginViewModel.kt | 29 +++++++--- .../app/omnivore/omnivore/MainActivity.kt | 53 ++++++++++++++++--- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt index 5b94ee716..ea22d4c38 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt @@ -13,10 +13,6 @@ import kotlinx.coroutines.flow.map import java.io.IOException import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore( - name = Constants.dataStoreName -) - interface DatastoreRepository { val hasAuthTokenFlow: Flow suspend fun clear() @@ -29,6 +25,10 @@ interface DatastoreRepository { class OmnivoreDatastore @Inject constructor( private val context: Context ) : DatastoreRepository { + private val Context.dataStore: DataStore by preferencesDataStore( + name = Constants.dataStoreName + ) + override suspend fun putString(key: String, value: String) { val preferencesKey = stringPreferencesKey(key) context.dataStore.edit { preferences -> diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt index 48a624862..e9102680a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt @@ -15,6 +15,12 @@ import javax.inject.Inject class LoginViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository ): ViewModel() { + var isLoading by mutableStateOf(false) + private set + + var errorMessage by mutableStateOf(null) + private set + val hasAuthTokenLiveData: LiveData = datastoreRepo .hasAuthTokenFlow .distinctUntilChanged() @@ -24,17 +30,24 @@ class LoginViewModel @Inject constructor( val emailLogin = RetrofitHelper.getInstance().create(EmailLoginSubmit::class.java) viewModelScope.launch { + isLoading = true + errorMessage = null + val result = emailLogin.submitEmailLogin( EmailLoginCredentials(email = email, password = password) ) - // TODO: bail early if email is pending -// if (result.body()?.pendingEmailVerification == true) { -// return void -// } + isLoading = false + + if (result.body()?.pendingEmailVerification == true) { + errorMessage = "Email needs verification" + return@launch + } if (result.body()?.authToken != null) { datastoreRepo.putString(DatastoreKeys.omnivoreAuthToken, result.body()?.authToken!!) + } else { + errorMessage = "Something went wrong. Please check your email/password and try again" } if (result.body()?.authCookieString != null) { @@ -42,10 +55,6 @@ class LoginViewModel @Inject constructor( DatastoreKeys.omnivoreAuthCookieString, result.body()?.authCookieString!! ) } - - datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken)?.let { - Log.d(ContentValues.TAG, it) - } } } @@ -54,4 +63,8 @@ class LoginViewModel @Inject constructor( datastoreRepo.clear() } } + + fun resetErrorMessage() { + errorMessage = null + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index 73d97853c..8d035d34b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -1,17 +1,19 @@ package app.omnivore.omnivore +import android.annotation.SuppressLint import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,10 +23,12 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import app.omnivore.omnivore.ui.theme.OmnivoreTheme -import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.setValue +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -37,15 +41,24 @@ class MainActivity : ComponentActivity() { OmnivoreTheme { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { - RootView(viewModel) + PrimaryView(viewModel) } } } + + // animate the view up when keyboard appears + WindowCompat.setDecorFitsSystemWindows(window, false) + val rootView = findViewById(android.R.id.content).rootView + ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> + val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + rootView.setPadding(0, 0, 0, imeHeight) + insets + } } } @Composable -fun RootView(viewModel: LoginViewModel) { +fun PrimaryView(viewModel: LoginViewModel) { val hasAuthToken: Boolean by viewModel.hasAuthTokenLiveData.observeAsState(false) if (hasAuthToken) { @@ -74,10 +87,14 @@ fun LoggedInView(viewModel: LoginViewModel) { } } +@SuppressLint("CoroutineCreationDuringComposition") @Composable fun LoginView(viewModel: LoginViewModel) { var email by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } + val focusManager = LocalFocusManager.current + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() Column( verticalArrangement = Arrangement.Center, @@ -85,6 +102,7 @@ fun LoginView(viewModel: LoginViewModel) { modifier = Modifier .background(MaterialTheme.colors.background) .fillMaxSize() + .clickable { focusManager.clearFocus() } ) { LoginFields( email, @@ -93,6 +111,27 @@ fun LoginView(viewModel: LoginViewModel) { onPasswordChange = { password = it }, onLoginClick = { viewModel.login(email, password) } ) + + // TODO: add a activity indicator (maybe after a delay?) + if (viewModel.isLoading) { + Text("Loading...") + } + + if (viewModel.errorMessage != null) { + coroutineScope.launch { + val result = snackBarHostState + .showSnackbar( + viewModel.errorMessage!!, + actionLabel = "Dismiss", + duration = SnackbarDuration.Indefinite + ) + when (result) { + SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage() + } + } + + SnackbarHost(hostState = snackBarHostState) + } } } @@ -142,7 +181,7 @@ fun LoginFields( } else { Toast.makeText( context, - "Please enter an email and password", + "Please enter an email address and password.", Toast.LENGTH_SHORT ).show() }