Repository: word-view/WordView-android
Pull request #50: replace toast msg from login class to snackbar message
Author: login: Niks2409
Labels: name: enhancement
description: New feature or request
Created: 2025-04-19T14:58:54Z | Merged: 2025-04-24T12:36:04Z

### Summary
- Replace toast message from loginViewModel to snackbar.
- Add Extension function for snackBar to common use.

### Attached Screenshot
![Screenshot 2025-04-19 202428](https://github.com/user-attachments/assets/1227d14e-365d-461a-9eb0-c7a5856d2d56)
![Screenshot 2025-04-19 202447](https://github.com/user-attachments/assets/53b084d4-b32e-4eee-9fef-9a1ab876ccd8)
![Screenshot 2025-04-19 202512](https://github.com/user-attachments/assets/d8cf2c2e-9e9d-4f53-94c5-60e408610ba0)

### Test Code
- Testing done using Login screen with wrong cred. 


Diff:
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 00000000..371f2e29
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="AppInsightsSettings">
+    <option name="tabSettings">
+      <map>
+        <entry key="Firebase Crashlytics">
+          <value>
+            <InsightsFilterSettings>
+              <option name="connection">
+                <ConnectionSetting>
+                  <option name="appId" value="PLACEHOLDER" />
+                  <option name="mobileSdkAppId" value="" />
+                  <option name="projectId" value="" />
+                  <option name="projectNumber" value="" />
+                </ConnectionSetting>
+              </option>
+              <option name="signal" value="SIGNAL_UNSPECIFIED" />
+              <option name="timeIntervalDays" value="THIRTY_DAYS" />
+              <option name="visibilityType" value="ALL" />
+            </InsightsFilterSettings>
+          </value>
+        </entry>
+      </map>
+    </option>
+  </component>
+</project>
\ No newline at end of file
diff --git a/app/src/androidTest/java/cc/wordview/app/ui/screens/login/LoginTest.kt b/app/src/androidTest/java/cc/wordview/app/ui/screens/login/LoginTest.kt
index a2903fbf..eb79738e 100644
--- a/app/src/androidTest/java/cc/wordview/app/ui/screens/login/LoginTest.kt
+++ b/app/src/androidTest/java/cc/wordview/app/ui/screens/login/LoginTest.kt
@@ -81,6 +81,7 @@ class LoginTest {
         composeTestRule.onNodeWithText("Log in")
             .assertIsEnabled()
             .performClick()
+        composeTestRule.onNodeWithText("Register failed!").assertDoesNotExist()
     }
 
     @Test
@@ -91,6 +92,8 @@ class LoginTest {
         composeTestRule.onNodeWithText("Log in")
             .assertIsEnabled()
             .performClick()
+
+        composeTestRule.onNodeWithText("This email address has not yet been registered").assertExists()
     }
 
     @Test
@@ -101,5 +104,7 @@ class LoginTest {
         composeTestRule.onNodeWithText("Log in")
             .assertIsEnabled()
             .performClick()
+
+        composeTestRule.onNodeWithText("Incorrect credentials").assertExists()
     }
 }
\ No newline at end of file
diff --git a/app/src/androidTest/java/cc/wordview/app/ui/screens/register/RegisterTest.kt b/app/src/androidTest/java/cc/wordview/app/ui/screens/register/RegisterTest.kt
index 34ed7645..f12f3cfb 100644
--- a/app/src/androidTest/java/cc/wordview/app/ui/screens/register/RegisterTest.kt
+++ b/app/src/androidTest/java/cc/wordview/app/ui/screens/register/RegisterTest.kt
@@ -117,6 +117,8 @@ class RegisterTest {
         composeTestRule.onNodeWithText("Create")
             .assertIsEnabled()
             .performClick()
+
+        composeTestRule.onNodeWithText("Register failed!").assertDoesNotExist()
     }
 
     @Test
@@ -129,5 +131,7 @@ class RegisterTest {
         composeTestRule.onNodeWithText("Create")
             .assertIsEnabled()
             .performClick()
+
+        composeTestRule.onNodeWithText("Register failed!").assertExists()
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt b/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt
index e8bdb4e4..cfab7734 100644
--- a/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt
+++ b/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt
@@ -28,12 +28,17 @@ import androidx.compose.material.icons.filled.Password
 import androidx.compose.material3.Button
 import androidx.compose.material3.FilledTonalButton
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -46,15 +51,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.rememberNavController
-import cc.wordview.app.ui.activities.auth.composables.FormValidation.*
+import cc.wordview.app.ui.activities.auth.composables.FormValidation.Email
 import cc.wordview.app.ui.activities.auth.viewmodel.login.LoginViewModel
 import cc.wordview.app.ui.activities.home.HomeActivity
-import cc.wordview.app.ui.activities.player.PlayerActivity
 import cc.wordview.app.ui.components.AuthForm
 import cc.wordview.app.ui.components.CircularProgressIndicator
 import cc.wordview.app.ui.components.FormTextField
 import cc.wordview.app.ui.components.Icon
+import cc.wordview.app.ui.components.OneTimeEffect
 import cc.wordview.app.ui.components.Space
+import kotlinx.coroutines.launch
 
 @Composable
 @Preview
@@ -69,7 +75,21 @@ fun Login(
 
     val context = LocalContext.current
 
-    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+    val snackBarHostState = remember { SnackbarHostState() }
+    val scope = rememberCoroutineScope()
+    val message by viewModel.snackBarMessage.collectAsState(initial = "")
+
+    LaunchedEffect(Unit) {
+        viewModel.snackBarMessage.collect {
+            scope.launch {
+                snackBarHostState.showSnackbar(message)
+            }
+        }
+    }
+
+    Scaffold(
+        modifier = Modifier.fillMaxSize(),
+        snackbarHost = { SnackbarHost(snackBarHostState) }) { innerPadding ->
         Box(
             modifier = Modifier
                 .fillMaxSize()
diff --git a/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Register.kt b/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Register.kt
index c15e9757..c211b628 100644
--- a/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Register.kt
+++ b/app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Register.kt
@@ -29,11 +29,16 @@ import androidx.compose.material.icons.filled.Password
 import androidx.compose.material3.Button
 import androidx.compose.material3.FilledTonalButton
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -46,14 +51,17 @@ import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.rememberNavController
-import cc.wordview.app.ui.activities.auth.composables.FormValidation.*
+import cc.wordview.app.ui.activities.auth.composables.FormValidation.Email
+import cc.wordview.app.ui.activities.auth.composables.FormValidation.Password
 import cc.wordview.app.ui.activities.auth.viewmodel.register.RegisterViewModel
 import cc.wordview.app.ui.activities.home.HomeActivity
 import cc.wordview.app.ui.components.AuthForm
 import cc.wordview.app.ui.components.CircularProgressIndicator
 import cc.wordview.app.ui.components.FormTextField
 import cc.wordview.app.ui.components.Icon
+import cc.wordview.app.ui.components.OneTimeEffect
 import cc.wordview.app.ui.components.Space
+import kotlinx.coroutines.launch
 
 @Composable
 @Preview
@@ -69,7 +77,21 @@ fun Register(
     val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
     val context = LocalContext.current
 
-    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+    val snackBarHostState = remember { SnackbarHostState() }
+    val scope = rememberCoroutineScope()
+    val message by viewModel.snackBarMessage.collectAsState(initial = "")
+
+    LaunchedEffect(Unit) {
+        viewModel.snackBarMessage.collect {
+            scope.launch {
+                snackBarHostState.showSnackbar(message)
+            }
+        }
+    }
+
+    Scaffold(
+        modifier = Modifier.fillMaxSize(),
+        snackbarHost = { SnackbarHost(snackBarHostState) }) { innerPadding ->
         Box(
             modifier = Modifier
                 .fillMaxSize()
diff --git a/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/login/LoginViewModel.kt b/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/login/LoginViewModel.kt
index ec5da52d..a02cc1e5 100644
--- a/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/login/LoginViewModel.kt
+++ b/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/login/LoginViewModel.kt
@@ -22,9 +22,10 @@ import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import cc.wordview.app.api.setStoredJwt
-import cc.wordview.app.extensions.showToast
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
@@ -37,34 +38,43 @@ class LoginViewModel @Inject constructor(
     private val loginRepository: LoginRepository
 ) : ViewModel() {
     private val _isLoading = MutableStateFlow(false)
-
     val isLoading = _isLoading.asStateFlow()
 
-    fun login(email: String, password: String, onLoginCompleted: () -> Unit, context: Context) = viewModelScope.launch {
-        _isLoading.update { true }
+    private var _snackBarMessage = MutableSharedFlow<String>()
+    val snackBarMessage = _snackBarMessage.asSharedFlow()
 
-        loginRepository.apply {
-            init(context)
+    fun login(email: String, password: String, onLoginCompleted: () -> Unit, context: Context) =
+        viewModelScope.launch {
+            _isLoading.update { true }
 
-            onSucceed = {
-                Timber.e("Login succeeded! jwt=$it")
-                setStoredJwt(it, context)
-                _isLoading.update { false }
-                onLoginCompleted.invoke()
-            }
-            onFail = { s: String, i: Int ->
-                Timber.e("Login failed \n\t message=$s, status=$i")
+            loginRepository.apply {
+                init(context)
+
+                onSucceed = {
+                    Timber.e("Login succeeded! jwt=$it")
+                    setStoredJwt(it, context)
+                    _isLoading.update { false }
+                    onLoginCompleted.invoke()
+                }
+                onFail = { s: String, i: Int ->
+                    Timber.e("Login failed \n\t message=$s, status=$i")
+
+                    if (s.startsWith("NoSuchEntryException")) {
+                        emitMessage("This email address has not yet been registered")
+                    } else if (s.startsWith("IncorrectCredentialsException")) {
+                        emitMessage("Incorrect credentials")
+                    }
 
-                if (s.startsWith("NoSuchEntryException")) {
-                    context.showToast("This email address has not yet been registered")
-                } else if (s.startsWith("IncorrectCredentialsException")) {
-                    context.showToast("Incorrect credentials")
+                    _isLoading.update { false }
                 }
 
-                _isLoading.update { false }
+                login(email, password)
             }
+        }
 
-            login(email, password)
+    private fun emitMessage(msg: String) {
+        viewModelScope.launch {
+            _snackBarMessage.emit(msg)
         }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/register/RegisterViewModel.kt b/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/register/RegisterViewModel.kt
index 2a4150c3..016dbf4a 100644
--- a/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/register/RegisterViewModel.kt
+++ b/app/src/main/java/cc/wordview/app/ui/activities/auth/viewmodel/register/RegisterViewModel.kt
@@ -21,9 +21,10 @@ import android.content.Context
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import cc.wordview.app.api.setStoredJwt
-import cc.wordview.app.extensions.showToast
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
@@ -35,9 +36,11 @@ class RegisterViewModel @Inject constructor(
     private val registerRepository: RegisterRepository
 ) : ViewModel() {
     private val _isLoading = MutableStateFlow(false)
-
     val isLoading = _isLoading.asStateFlow()
 
+    private var _snackBarMessage = MutableSharedFlow<String>()
+    val snackBarMessage = _snackBarMessage.asSharedFlow()
+
     fun register(
         username: String,
         email: String,
@@ -57,14 +60,18 @@ class RegisterViewModel @Inject constructor(
                 onRegisterCompleted.invoke()
             }
             onFail = { s: String, i: Int ->
-
                 Timber.e("Register failed \n\t message=$s, status=$i")
-                context.showToast("Register failed!")
-
+                emitMessage("Register failed!")
                 _isLoading.update { false }
             }
 
             register(username, email, password)
         }
     }
+
+    private fun emitMessage(msg: String) {
+        viewModelScope.launch {
+            _snackBarMessage.emit(msg)
+        }
+    }
 }
\ No newline at end of file


Reviews:
user: login: 64ArthurAraujo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T20:13:16Z
user: login: Niks2409
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T09:39:01Z
user: login: Niks2409
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T10:29:14Z

Review comments:
diff_hunk: @@ -69,7 +73,22 @@ fun Login(
 
     val context = LocalContext.current
 
-    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+    val snackBarHostState = remember { SnackbarHostState() } // ✅ This line is essential!
+    val scope = rememberCoroutineScope()
+
+    // 🔁 Collect events from ViewModel
+    LaunchedEffect(Unit) {
+        viewModel.snackBarMessage.collect { message ->
+            // ✅ You can use `scope.launch` here, but it's optional
+            scope.launch {
+                snackBarHostState.showSnackbar(message)
+            }
+        }
+    }
path: app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt
user: login: 64ArthurAraujo
body: Instead of LaunchedEffect(Unit) i use OneTimeEffect, it does the same thing but with a better nomenclature 
created_at: 2025-04-19T20:12:56Z
author_association: MEMBER
diff_hunk: @@ -69,7 +73,22 @@ fun Login(
 
     val context = LocalContext.current
 
-    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+    val snackBarHostState = remember { SnackbarHostState() } // ✅ This line is essential!
+    val scope = rememberCoroutineScope()
+
+    // 🔁 Collect events from ViewModel
+    LaunchedEffect(Unit) {
+        viewModel.snackBarMessage.collect { message ->
+            // ✅ You can use `scope.launch` here, but it's optional
+            scope.launch {
+                snackBarHostState.showSnackbar(message)
+            }
+        }
+    }
path: app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt
user: login: Niks2409
body: changes done please review 
created_at: 2025-04-20T09:39:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,7 +73,22 @@ fun Login(
 
     val context = LocalContext.current
 
-    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+    val snackBarHostState = remember { SnackbarHostState() } // ✅ This line is essential!
+    val scope = rememberCoroutineScope()
+
+    // 🔁 Collect events from ViewModel
+    LaunchedEffect(Unit) {
+        viewModel.snackBarMessage.collect { message ->
+            // ✅ You can use `scope.launch` here, but it's optional
+            scope.launch {
+                snackBarHostState.showSnackbar(message)
+            }
+        }
+    }
path: app/src/main/java/cc/wordview/app/ui/activities/auth/composables/Login.kt
user: login: Niks2409
body: registration screen toast message replace to sanckbar as well 
created_at: 2025-04-20T10:29:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: 64ArthurAraujo
created_at: 2025-04-19T19:41:27Z
body: Should also include the snackbar for the register screen
author_association: MEMBER
user: login: Niks2409
created_at: 2025-04-22T04:29:36Z
body: Is there any thing need to update ??
author_association: CONTRIBUTOR
user: login: 64ArthurAraujo
created_at: 2025-04-22T11:19:43Z
body: > Is there any thing need to update ??

Hello! No, I'm not able to merge rn, I will be able to check again in a couple of days
author_association: MEMBER