Repository: Automattic/Gravatar-SDK-Android
Pull request #630: Show Unsaved Changes alert dialog on modal dismissal actions (back button, tap outside, drag down).
Author: login: AdamGrzybkowski
Labels: name: [Feature] Gravatar-Quickeditor
description: Gravatar Quick Editor module
Created: 2025-05-15T11:21:49Z | Merged: 2025-05-16T13:44:25Z

### Description

This PR extends the number of triggers for the Unsaved Changes alert dialog. It will now be shown not only on `Done` button tap but also on any actions that trigger Modal dismissal. 

In order to do that, I had to update the Compose Unstyled library to have access to the `confirmDetentChange()` callback that was added and does exactly what we need.

Additionally, I had to:
- Remove the custom click handled on the `Scrim`
- Remove the custom `BackHandler`
Those two are now handled by the Modal sheet directly and hence are now routed via `confirmDetentChange` callback that we use. 

On top of that I had to change a bit where we create the `ModalBottomSheetState` as we need it in a certain place for the `confirmDetentChange` callback. 

Now to the actual implementation part: 

I ended up on a Plan B solution - that is to pass down a flag when the user tried to dismiss that dialog. When such a flag is received the `QuickEditorViewModel` will check the page and delegate the unsaved changes check.

Next to the flag we also need a new callback `onDismissIgnored()` to make sure we clear that flag when the user decide to "Keep editing" so that it will be possible to send it again later.


https://github.com/user-attachments/assets/cf998a01-fd0d-4071-bd02-d920c914fd5b

 

### Testing Steps

1. Launch the QE with the About scope or (AvatarAndAbout)
2. Try to close the QE using all the mentioned actions
3. Confirm it was closed fine
4. Open QE again
5. Update the profile (without saving)
6. Try to close the QE using all the mentioned actions
7. Tap `Keep editting`
8. Confirm you're back in QE, and the alert reappears every time
9. Try to dismiss it again
10. Tap `Exit`
11. Confirm the QE was closed
12. Run some basic tests for the Avatar scope as well to make sure it closes as before



Diff:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3f95073fc..e6ac6bdb2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,7 +7,7 @@ appcompat = "1.7.0"
 binaryValidator = "0.15.0-Beta.2"
 browser = "1.8.0"
 coil = "2.7.0"
-composablesCore = "1.15.0"
+composablesCore = "1.30.0"
 composeBom = "2024.02.00"
 composeUiTest = "1.7.2"
 constraintlayout = "2.1.4"
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditor.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditor.kt
index 932e6e287..f7ff685d5 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditor.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditor.kt
@@ -48,6 +48,7 @@ import kotlinx.coroutines.withContext
 internal fun AboutEditor(
     quickEditorParams: GravatarQuickEditorParams,
     onProfileUpdated: (Profile) -> Unit,
+    onDismissIgnored: () -> Unit,
     onClose: () -> Unit,
     viewModel: AboutEditorViewModel = viewModel(
         factory = AboutEditorViewModelFactory(quickEditorParams),
@@ -91,6 +92,8 @@ internal fun AboutEditor(
                         AboutEditorAction.CloseEditor -> {
                             onClose()
                         }
+
+                        AboutEditorAction.NotifyDismissIgnored -> onDismissIgnored()
                     }
                 }
             }
@@ -114,11 +117,10 @@ internal fun AboutEditor(
             DiscardChangesAlertDialog(
                 visible = uiState.discardChangesDialogVisible,
                 onKeepEditing = {
-                    viewModel.onEvent(AboutEditorEvent.OnDiscardDialogDismissed)
+                    viewModel.onEvent(AboutEditorEvent.OnUnsavedChangesKeepEditingClicked)
                 },
                 onDiscardClicked = {
-                    viewModel.onEvent(AboutEditorEvent.OnDiscardConfirmed)
-                    onClose()
+                    viewModel.onEvent(AboutEditorEvent.OnUnsavedChangesExitClicked)
                 },
             )
         }
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorAction.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorAction.kt
index 0632c2e9b..8e0d86989 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorAction.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorAction.kt
@@ -8,4 +8,6 @@ internal sealed class AboutEditorAction {
     data object ProfileUpdateFailed : AboutEditorAction()
 
     data object CloseEditor : AboutEditorAction()
+
+    data object NotifyDismissIgnored : AboutEditorAction()
 }
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorEvent.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorEvent.kt
index 194c0d8cb..7eb590b56 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorEvent.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorEvent.kt
@@ -9,7 +9,7 @@ internal sealed class AboutEditorEvent {
 
     data object OnDoneClicked : AboutEditorEvent()
 
-    data object OnDiscardDialogDismissed : AboutEditorEvent()
+    data object OnUnsavedChangesKeepEditingClicked : AboutEditorEvent()
 
-    data object OnDiscardConfirmed : AboutEditorEvent()
+    data object OnUnsavedChangesExitClicked : AboutEditorEvent()
 }
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
index ed46453a4..163961b3f 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
@@ -40,23 +40,34 @@ internal class AboutEditorViewModel(
             is AboutEditorEvent.OnAboutFieldUpdated -> updateAboutField(aboutEditorEvent.aboutField)
             AboutEditorEvent.OnSaveClicked -> saveProfile()
             AboutEditorEvent.OnDoneClicked -> checkForUnsavedChanges()
-            AboutEditorEvent.OnDiscardDialogDismissed,
-            AboutEditorEvent.OnDiscardConfirmed,
-            -> dismissDiscardChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesKeepEditingClicked -> dismissUnsavedChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesExitClicked -> discardUnsavedChangesDialog()
         }
     }
 
-    private fun dismissDiscardChangesDialog() {
+    private fun discardUnsavedChangesDialog() {
+        viewModelScope.launch {
+            _actions.send(AboutEditorAction.CloseEditor)
+        }
+        _uiState.update { currentState ->
+            currentState.copy(discardChangesDialogVisible = false)
+        }
+    }
+
+    private fun dismissUnsavedChangesDialog() {
+        viewModelScope.launch {
+            _actions.send(AboutEditorAction.NotifyDismissIgnored)
+        }
         _uiState.update { currentState ->
             currentState.copy(discardChangesDialogVisible = false)
         }
     }
 
     private fun checkForUnsavedChanges() {
-        val currentProfile = uiState.value.aboutFields
-        val savedProfile = savedProfile?.aboutFields
         viewModelScope.launch {
-            if (currentProfile != savedProfile) {
+            val currentProfile = uiState.value.aboutFields
+            val savedProfile = savedProfile?.aboutFields
+            if (savedProfile != null && currentProfile != savedProfile) {
                 _uiState.update { currentProfile ->
                     currentProfile.copy(
                         discardChangesDialogVisible = true,
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/components/AboutEditField.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/components/AboutEditField.kt
index c08e4bf4e..deb1b0dd0 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/components/AboutEditField.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/components/AboutEditField.kt
@@ -1,6 +1,5 @@
 package com.gravatar.quickeditor.ui.abouteditor.components
 
-import android.util.Log
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -46,10 +45,7 @@ internal fun AboutEditField(
             maxLines = maxLines,
             singleLine = maxLines == 1,
             enabled = enabled,
-            onValueChange = {
-                Log.d("QuickEditor", "onValueChange: $it")
-                onValueChange(it)
-            },
+            onValueChange = onValueChange,
             decorationBox = { innerTextField ->
                 Box(
                     modifier = Modifier
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/GravatarQuickEditorPage.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/GravatarQuickEditorPage.kt
index f3c0d3c96..b95ed6fb3 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/GravatarQuickEditorPage.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/GravatarQuickEditorPage.kt
@@ -30,6 +30,8 @@ import com.gravatar.quickeditor.ui.splash.SplashPage
  * @param oAuthParams The OAuth parameters.
  * @param updateHandler The callback for the Quick Editor updates.
  *                       Can be invoked multiple times while the Quick Editor is open
+ * @param confirmDismissal Whether to show the confirmation dialog when dismissing the Quick Editor.
+ * @param onDismissIgnored The callback for the dismiss action when the Quick Editor is ignored.
  * @param onDoneClicked The callback for the done action.
  * @param onDismiss The callback for the dismiss action.
  *                  [GravatarQuickEditorError] will be non-null if the dismiss was caused by an error.
@@ -39,6 +41,8 @@ internal fun GravatarQuickEditorPage(
     gravatarQuickEditorParams: GravatarQuickEditorParams,
     oAuthParams: OAuthParams,
     updateHandler: UpdateHandler,
+    confirmDismissal: Boolean,
+    onDismissIgnored: () -> Unit,
     onDoneClicked: () -> Unit,
     onDismiss: (dismissReason: GravatarQuickEditorDismissReason) -> Unit = {},
 ) {
@@ -73,11 +77,13 @@ internal fun GravatarQuickEditorPage(
                 onDoneClicked = onDoneClicked,
             )
         }
-        addAvatarPickerGraph(
+        addEditorGraph(
             gravatarQuickEditorParams = gravatarQuickEditorParams,
             handleExpiredSession = true,
             navController = navController,
             updateHandler = updateHandler,
+            confirmDismissal = confirmDismissal,
+            onDismissIgnored = onDismissIgnored,
             onSessionExpired = {
                 navController.navigateAndPopupTo(QuickEditorPage.OAUTH.name, QuickEditorPage.EDITOR.name)
             },
@@ -94,6 +100,8 @@ internal fun GravatarQuickEditorPage(
  * @param authToken The authentication token.
  * @param updateHandler The callback for the Quick Editor updates.
  *                       Can be invoked multiple times while the Quick Editor is open
+ * @param confirmDismissal Whether to show the confirmation dialog when dismissing the Quick Editor.
+ * @param onDismissIgnored The callback for the dismiss action when the Quick Editor is ignored.
  * @param onDoneClicked The callback for the done action.
  * @param onDismiss The callback for the dismiss action.
  *                  [GravatarQuickEditorError] will be non-null if the dismiss was caused by an error.
@@ -103,6 +111,8 @@ internal fun GravatarQuickEditorPage(
     gravatarQuickEditorParams: GravatarQuickEditorParams,
     authToken: String,
     updateHandler: UpdateHandler,
+    confirmDismissal: Boolean,
+    onDismissIgnored: () -> Unit,
     onDoneClicked: () -> Unit,
     onDismiss: (dismissReason: GravatarQuickEditorDismissReason) -> Unit = {},
 ) {
@@ -123,11 +133,13 @@ internal fun GravatarQuickEditorPage(
                 navController.navigateAndPopupTo(QuickEditorPage.EDITOR.name, QuickEditorPage.SPLASH.name)
             }
         }
-        addAvatarPickerGraph(
+        addEditorGraph(
             gravatarQuickEditorParams = gravatarQuickEditorParams,
             handleExpiredSession = false,
             navController = navController,
             updateHandler = updateHandler,
+            confirmDismissal = confirmDismissal,
+            onDismissIgnored = onDismissIgnored,
             onSessionExpired = { onDismiss(GravatarQuickEditorDismissReason.InvalidToken) },
             onDoneClicked = onDoneClicked,
         )
@@ -135,11 +147,13 @@ internal fun GravatarQuickEditorPage(
 }
 
 @Suppress("LongParameterList")
-private fun NavGraphBuilder.addAvatarPickerGraph(
+private fun NavGraphBuilder.addEditorGraph(
     navController: NavHostController,
     gravatarQuickEditorParams: GravatarQuickEditorParams,
     handleExpiredSession: Boolean,
     updateHandler: UpdateHandler,
+    confirmDismissal: Boolean,
+    onDismissIgnored: () -> Unit,
     onSessionExpired: () -> Unit,
     onDoneClicked: () -> Unit,
 ) {
@@ -159,6 +173,8 @@ private fun NavGraphBuilder.addAvatarPickerGraph(
                 gravatarQuickEditorParams = gravatarQuickEditorParams,
                 handleExpiredSession = handleExpiredSession,
                 updateHandler = updateHandler,
+                confirmDismissal = confirmDismissal,
+                onDismissIgnored = onDismissIgnored,
                 onSessionExpired = onSessionExpired,
                 onAltTextTapped = { email, avatarId ->
                     navController.navigate(
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditor.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditor.kt
index 29f635cc6..c3e6929c6 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditor.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditor.kt
@@ -10,10 +10,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.gravatar.quickeditor.ui.abouteditor.AboutEditor
 import com.gravatar.quickeditor.ui.abouteditor.AboutEditorEvent
@@ -24,12 +28,17 @@ import com.gravatar.quickeditor.ui.components.EmailLabel
 import com.gravatar.quickeditor.ui.components.ProfileCard
 import com.gravatar.quickeditor.ui.components.QEPageDefault
 import com.gravatar.ui.GravatarTheme
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.withContext
 
 @Composable
 internal fun QuickEditor(
     gravatarQuickEditorParams: GravatarQuickEditorParams,
     handleExpiredSession: Boolean,
     updateHandler: UpdateHandler,
+    confirmDismissal: Boolean,
+    onDismissIgnored: () -> Unit,
     onSessionExpired: () -> Unit,
     onDoneClicked: () -> Unit,
     onAltTextTapped: (email: String, avatarId: String) -> Unit,
@@ -37,19 +46,39 @@ internal fun QuickEditor(
         factory = QuickEditorViewModelFactory(gravatarQuickEditorParams),
     ),
 ) {
+    val lifecycle = LocalLifecycleOwner.current.lifecycle
     val uiState by viewModel.uiState.collectAsState()
     val aboutEditorViewModel: AboutEditorViewModel = viewModel(
         factory = AboutEditorViewModelFactory(gravatarQuickEditorParams),
     )
 
+    LaunchedEffect(confirmDismissal) {
+        if (confirmDismissal) {
+            viewModel.onEvent(QuickEditorEvent.OnConfirmDismissal)
+        }
+    }
+
+    LaunchedEffect(Unit) {
+        withContext(Dispatchers.Main.immediate) {
+            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.actions.collectLatest { action ->
+                    when (action) {
+                        QuickEditorAction.ConfirmEditorDismissal -> {
+                            aboutEditorViewModel.onEvent(AboutEditorEvent.OnDoneClicked)
+                        }
+
+                        QuickEditorAction.DismissEditor -> {
+                            onDoneClicked()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     QuickEditor(
         uiState = uiState,
-        onDoneClicked = {
-            when (uiState.page) {
-                QuickEditorPage.AvatarPicker -> onDoneClicked()
-                QuickEditorPage.AboutEditor -> aboutEditorViewModel.onEvent(AboutEditorEvent.OnDoneClicked)
-            }
-        },
+        onDoneClicked = { viewModel.onEvent(QuickEditorEvent.OnConfirmDismissal) },
         onEditAvatarClicked = { viewModel.onEvent(QuickEditorEvent.OnEditAvatarClicked) },
         onEditAboutClicked = { viewModel.onEvent(QuickEditorEvent.OnEditAboutClicked) },
     ) {
@@ -80,11 +109,12 @@ internal fun QuickEditor(
                 QuickEditorPage.AboutEditor -> {
                     AboutEditor(
                         quickEditorParams = gravatarQuickEditorParams,
+                        onDismissIgnored = onDismissIgnored,
                         onProfileUpdated = { profile ->
                             updateHandler(AboutEditorResult(profile))
                             viewModel.onEvent(QuickEditorEvent.OnProfileUpdated(profile))
                         },
-                        onClose = onDoneClicked,
+                        onClose = { onDoneClicked() },
                         viewModel = aboutEditorViewModel,
                     )
                 }
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorAction.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorAction.kt
new file mode 100644
index 000000000..0c084d303
--- /dev/null
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorAction.kt
@@ -0,0 +1,7 @@
+package com.gravatar.quickeditor.ui.editor
+
+internal sealed class QuickEditorAction {
+    data object ConfirmEditorDismissal : QuickEditorAction()
+
+    data object DismissEditor : QuickEditorAction()
+}
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorEvent.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorEvent.kt
index bb16bb029..ebc7888ca 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorEvent.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorEvent.kt
@@ -12,4 +12,6 @@ internal sealed class QuickEditorEvent {
     data object OnEditAvatarClicked : QuickEditorEvent()
 
     data object OnEditAboutClicked : QuickEditorEvent()
+
+    data object OnConfirmDismissal : QuickEditorEvent()
 }
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModel.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModel.kt
index 20912b8a6..187184a86 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModel.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModel.kt
@@ -14,9 +14,12 @@ import com.gravatar.quickeditor.ui.time.SystemClock
 import com.gravatar.services.GravatarResult
 import com.gravatar.types.Email
 import com.gravatar.ui.components.ComponentState
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 
@@ -37,6 +40,9 @@ internal class QuickEditorViewModel(
     )
     val uiState: StateFlow<QuickEditorUiState> = _uiState.asStateFlow()
 
+    private val _actions = Channel<QuickEditorAction>(Channel.BUFFERED)
+    val actions = _actions.receiveAsFlow()
+
     init {
         refresh()
     }
@@ -54,6 +60,14 @@ internal class QuickEditorViewModel(
 
             QuickEditorEvent.OnEditAboutClicked -> navigateToPage(QuickEditorPage.AboutEditor)
             QuickEditorEvent.OnEditAvatarClicked -> navigateToPage(QuickEditorPage.AvatarPicker)
+            QuickEditorEvent.OnConfirmDismissal -> {
+                viewModelScope.launch(Dispatchers.Main.immediate) {
+                    when (uiState.value.page) {
+                        QuickEditorPage.AvatarPicker -> _actions.send(QuickEditorAction.DismissEditor)
+                        QuickEditorPage.AboutEditor -> _actions.send(QuickEditorAction.ConfirmEditorDismissal)
+                    }
+                }
+            }
         }
     }
 
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/bottomsheet/GravatarQuickEditorBottomSheet.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/bottomsheet/GravatarQuickEditorBottomSheet.kt
index 618e26ecb..2d0835bad 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/bottomsheet/GravatarQuickEditorBottomSheet.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/bottomsheet/GravatarQuickEditorBottomSheet.kt
@@ -2,9 +2,7 @@ package com.gravatar.quickeditor.ui.editor.bottomsheet
 
 import android.content.res.Configuration
 import android.graphics.Color
-import androidx.activity.compose.BackHandler
 import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.WindowInsets
@@ -25,7 +23,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
+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
 import androidx.compose.ui.draw.clip
@@ -33,7 +35,6 @@ import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.dp
 import androidx.core.view.WindowInsetsControllerCompat
 import androidx.window.core.layout.WindowHeightSizeClass
-import com.composables.core.LocalModalWindow
 import com.composables.core.ModalBottomSheet
 import com.composables.core.ModalBottomSheetState
 import com.composables.core.ModalSheetProperties
@@ -43,6 +44,7 @@ import com.composables.core.SheetDetent
 import com.composables.core.SheetDetent.Companion.FullyExpanded
 import com.composables.core.SheetDetent.Companion.Hidden
 import com.composables.core.rememberModalBottomSheetState
+import com.composeunstyled.LocalModalWindow
 import com.gravatar.quickeditor.QuickEditorContainer
 import com.gravatar.quickeditor.ui.components.QEDragHandle
 import com.gravatar.quickeditor.ui.editor.AuthenticationMethod
@@ -82,9 +84,7 @@ public fun GravatarQuickEditorBottomSheet(
         authenticationMethod = authenticationMethod,
         updateHandler = updateHandler,
         onDismiss = onDismiss,
-        modalBottomSheetState = rememberGravatarModalBottomSheetState(
-            avatarPickerContentLayout = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout,
-        ),
+        modalDetents = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout.modalDetents(),
     )
 }
 
@@ -120,28 +120,63 @@ public fun GravatarQuickEditorBottomSheet(
             }
         },
         onDismiss = onDismiss,
-        modalBottomSheetState = rememberGravatarModalBottomSheetState(
-            avatarPickerContentLayout = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout,
-        ),
+        modalDetents = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout.modalDetents(),
     )
 }
 
+internal enum class DismissConfirmationState {
+    Delegate,
+    Confirm,
+    Delegated,
+}
+
 @Composable
 internal fun GravatarQuickEditorBottomSheet(
     gravatarQuickEditorParams: GravatarQuickEditorParams,
     authenticationMethod: AuthenticationMethod,
     updateHandler: UpdateHandler,
+    modalDetents: ModalDetents,
     onDismiss: (dismissReason: GravatarQuickEditorDismissReason) -> Unit = {},
-    modalBottomSheetState: ModalBottomSheetState,
+    onCurrentDetentChanged: (sheetDetent: SheetDetent) -> Unit = {},
 ) {
     val coroutineScope = rememberCoroutineScope()
+    var dismissState: DismissConfirmationState by remember { mutableStateOf(DismissConfirmationState.Delegate) }
+
+    val onDismissIgnored = {
+        dismissState = DismissConfirmationState.Delegate
+    }
+
+    val modalBottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
+        initialDetent = modalDetents.initialDetent,
+        detents = modalDetents.detents,
+        confirmDetentChange = { sheetDetent ->
+            // We only care about the Hidden state
+            if (sheetDetent == Hidden) {
+                when (dismissState) {
+                    DismissConfirmationState.Confirm -> true
+                    DismissConfirmationState.Delegated -> false
+                    DismissConfirmationState.Delegate -> {
+                        dismissState = DismissConfirmationState.Delegated
+                        false
+                    }
+                }
+            } else {
+                true
+            }
+        },
+    )
 
     val onDoneClicked: () -> Unit = {
+        dismissState = DismissConfirmationState.Confirm
         coroutineScope.launch {
-            modalBottomSheetState.currentDetent = Hidden
+            modalBottomSheetState.animateTo(Hidden)
         }
     }
 
+    LaunchedEffect(modalBottomSheetState.currentDetent) {
+        onCurrentDetentChanged(modalBottomSheetState.currentDetent)
+    }
+
     DisposableEffect(Unit) {
         if (authenticationMethod is AuthenticationMethod.Bearer) {
             QuickEditorContainer.getInstance().useInMemoryTokenStorage()
@@ -165,6 +200,8 @@ internal fun GravatarQuickEditorBottomSheet(
                         authToken = authenticationMethod.token,
                         onDismiss = onDismiss,
                         updateHandler = updateHandler,
+                        confirmDismissal = dismissState == DismissConfirmationState.Delegated,
+                        onDismissIgnored = onDismissIgnored,
                         onDoneClicked = onDoneClicked,
                     )
                 }
@@ -175,6 +212,8 @@ internal fun GravatarQuickEditorBottomSheet(
                         oAuthParams = authenticationMethod.oAuthParams,
                         onDismiss = onDismiss,
                         updateHandler = updateHandler,
+                        confirmDismissal = dismissState == DismissConfirmationState.Delegated,
+                        onDismissIgnored = onDismissIgnored,
                         onDoneClicked = onDoneClicked,
                     )
                 }
@@ -190,8 +229,6 @@ private fun GravatarModalBottomSheet(
     modalBottomSheetState: ModalBottomSheetState,
     content: @Composable () -> Unit,
 ) {
-    val coroutineScope = rememberCoroutineScope()
-
     LaunchedEffect(modalBottomSheetState.currentDetent) {
         if (modalBottomSheetState.currentDetent == Hidden) {
             onDismiss(GravatarQuickEditorDismissReason.Finished)
@@ -211,20 +248,17 @@ private fun GravatarModalBottomSheet(
         GravatarTheme {
             ModalBottomSheet(
                 state = modalBottomSheetState,
-                properties = ModalSheetProperties(dismissOnBackPress = false),
+                properties = ModalSheetProperties(
+                    dismissOnBackPress = true,
+                    dismissOnClickOutside = true,
+                ),
             ) {
-                BackHandler {
-                    coroutineScope.launch {
-                        modalBottomSheetState.animateTo(Hidden)
-                    }
-                }
                 // Modal content must be taking the uiMode from Activity and doesn't respect
                 // the above set CompositionLocalProvider
                 CompositionLocalProvider(
                     LocalConfiguration provides configuration,
                 ) {
                     Scrim(
-                        modifier = Modifier.clickable { modalBottomSheetState.currentDetent = Hidden },
                         scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f),
                     )
                     Sheet(
@@ -266,38 +300,40 @@ private fun GravatarModalBottomSheet(
     }
 }
 
+internal val peek = SheetDetent(identifier = "peek") { containerHeight, _ ->
+    containerHeight * 0.6f
+}
+
 @Composable
-internal fun rememberGravatarModalBottomSheetState(
-    avatarPickerContentLayout: AvatarPickerContentLayout,
-): ModalBottomSheetState {
+internal fun AvatarPickerContentLayout.modalDetents(): ModalDetents {
     val windowHeightSizeClass = currentWindowAdaptiveInfo().windowSizeClass.windowHeightSizeClass
-    val peek = SheetDetent(identifier = "peek") { containerHeight, _ ->
-        containerHeight * 0.6f
-    }
-
-    val initialDetent =
-        if (windowHeightSizeClass == WindowHeightSizeClass.COMPACT) {
-            FullyExpanded
-        } else {
-            when (avatarPickerContentLayout) {
-                AvatarPickerContentLayout.Horizontal -> FullyExpanded
-                AvatarPickerContentLayout.Vertical -> peek
-            }
+    val initialDetent = if (windowHeightSizeClass == WindowHeightSizeClass.COMPACT) {
+        FullyExpanded
+    } else {
+        when (this) {
+            AvatarPickerContentLayout.Horizontal -> FullyExpanded
+            AvatarPickerContentLayout.Vertical -> peek
         }
+    }
 
     val detents = buildList {
         add(Hidden)
-        if (avatarPickerContentLayout == AvatarPickerContentLayout.Horizontal) {
+        if (this@modalDetents == AvatarPickerContentLayout.Horizontal) {
             add(FullyExpanded)
         } else {
             add(peek)
             add(FullyExpanded)
         }
     }
-    return rememberModalBottomSheetState(
+    return ModalDetents(
         initialDetent = initialDetent,
         detents = detents,
     )
 }
 
+internal data class ModalDetents(
+    val initialDetent: SheetDetent,
+    val detents: List<SheetDetent>,
+)
+
 internal val DEFAULT_PAGE_HEIGHT = 250.dp
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/extensions/QuickEditorExtensions.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/extensions/QuickEditorExtensions.kt
index 71c20af91..d647d84c1 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/extensions/QuickEditorExtensions.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/extensions/QuickEditorExtensions.kt
@@ -1,10 +1,6 @@
 package com.gravatar.quickeditor.ui.editor.extensions
 
 import android.view.ViewGroup
-import androidx.activity.compose.BackHandler
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.platform.ComposeView
 import com.composables.core.SheetDetent.Companion.Hidden
 import com.gravatar.quickeditor.ui.editor.AuthenticationMethod
@@ -12,8 +8,7 @@ import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorDismissReason
 import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorParams
 import com.gravatar.quickeditor.ui.editor.UpdateHandler
 import com.gravatar.quickeditor.ui.editor.bottomsheet.GravatarQuickEditorBottomSheet
-import com.gravatar.quickeditor.ui.editor.bottomsheet.rememberGravatarModalBottomSheetState
-import kotlinx.coroutines.launch
+import com.gravatar.quickeditor.ui.editor.bottomsheet.modalDetents
 
 internal fun addQuickEditorToView(
     viewGroup: ViewGroup,
@@ -25,53 +20,19 @@ internal fun addQuickEditorToView(
     viewGroup.addView(
         ComposeView(viewGroup.context).apply {
             setContent {
-                GravatarQuickEditorBottomSheetWrapper(
-                    parent = viewGroup,
-                    composeView = this,
+                GravatarQuickEditorBottomSheet(
                     gravatarQuickEditorParams = gravatarQuickEditorParams,
                     authenticationMethod = authenticationMethod,
                     updateHandler = updateHandler,
                     onDismiss = onDismiss,
+                    modalDetents = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout.modalDetents(),
+                    onCurrentDetentChanged = {
+                        if (it == Hidden) {
+                            viewGroup.removeView(this)
+                        }
+                    },
                 )
             }
         },
     )
 }
-
-@Composable
-private fun GravatarQuickEditorBottomSheetWrapper(
-    parent: ViewGroup,
-    composeView: ComposeView,
-    gravatarQuickEditorParams: GravatarQuickEditorParams,
-    authenticationMethod: AuthenticationMethod,
-    updateHandler: UpdateHandler,
-    onDismiss: (dismissReason: GravatarQuickEditorDismissReason) -> Unit,
-) {
-    val coroutineScope = rememberCoroutineScope()
-
-    val modalBottomSheetState = rememberGravatarModalBottomSheetState(
-        avatarPickerContentLayout = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout,
-    )
-
-    GravatarQuickEditorBottomSheet(
-        gravatarQuickEditorParams = gravatarQuickEditorParams,
-        authenticationMethod = authenticationMethod,
-        updateHandler = updateHandler,
-        onDismiss = onDismiss,
-        modalBottomSheetState = modalBottomSheetState,
-    )
-
-    BackHandler {
-        coroutineScope.launch {
-            modalBottomSheetState.currentDetent = Hidden
-        }
-    }
-
-    LaunchedEffect(modalBottomSheetState.currentDetent) {
-        when (modalBottomSheetState.currentDetent) {
-            Hidden -> {
-                parent.removeView(composeView)
-            }
-        }
-    }
-}
diff --git a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModelTest.kt b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModelTest.kt
index 1e4d904a6..fa9a71537 100644
--- a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModelTest.kt
+++ b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModelTest.kt
@@ -195,7 +195,7 @@ class AboutEditorViewModelTest {
     }
 
     @Test
-    fun `given discard changes dialog shown when dismissed then hidden`() = runTest {
+    fun `given unsaved changes dialog shown when keep editing tapped then hidden`() = runTest {
         viewModel = initViewModel()
 
         viewModel.onEvent(
@@ -212,13 +212,17 @@ class AboutEditorViewModelTest {
         viewModel.uiState.test {
             assertEquals(true, awaitItem().discardChangesDialogVisible)
 
-            viewModel.onEvent(AboutEditorEvent.OnDiscardDialogDismissed)
+            viewModel.onEvent(AboutEditorEvent.OnUnsavedChangesKeepEditingClicked)
             assertEquals(false, awaitItem().discardChangesDialogVisible)
         }
+
+        viewModel.actions.test {
+            assertEquals(AboutEditorAction.NotifyDismissIgnored, awaitItem())
+        }
     }
 
     @Test
-    fun `given discard changes dialog shown when discard confirmed then hidden`() = runTest {
+    fun `given unsaved changes dialog shown when exit tapped then editor closed`() = runTest {
         viewModel = initViewModel()
 
         viewModel.onEvent(
@@ -235,9 +239,12 @@ class AboutEditorViewModelTest {
         viewModel.uiState.test {
             assertEquals(true, awaitItem().discardChangesDialogVisible)
 
-            viewModel.onEvent(AboutEditorEvent.OnDiscardConfirmed)
+            viewModel.onEvent(AboutEditorEvent.OnUnsavedChangesExitClicked)
             assertEquals(false, awaitItem().discardChangesDialogVisible)
         }
+        viewModel.actions.test {
+            assertEquals(AboutEditorAction.CloseEditor, awaitItem())
+        }
     }
 
     private fun initViewModel() = AboutEditorViewModel(
diff --git a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModelTest.kt b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModelTest.kt
index c652f7fb3..f3b076160 100644
--- a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModelTest.kt
+++ b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModelTest.kt
@@ -160,6 +160,30 @@ class QuickEditorViewModelTest {
         }
     }
 
+    @Test
+    fun `given current page is AvatarPicker when OnConfirmDismissal event then DismissEditor action is sent`() =
+        runTest {
+            viewModel = initViewModel(initialPage = QuickEditorPage.AvatarPicker)
+
+            viewModel.actions.test {
+                viewModel.onEvent(QuickEditorEvent.OnConfirmDismissal)
+
+                assertEquals(QuickEditorAction.DismissEditor, awaitItem())
+            }
+        }
+
+    @Test
+    fun `given current page is AboutEditor when OnConfirmDismissal event then ConfirmEditorDismissal action is sent`() =
+        runTest {
+            viewModel = initViewModel(initialPage = QuickEditorPage.AboutEditor)
+
+            viewModel.actions.test {
+                viewModel.onEvent(QuickEditorEvent.OnConfirmDismissal)
+
+                assertEquals(QuickEditorAction.ConfirmEditorDismissal, awaitItem())
+            }
+        }
+
     private fun initViewModel(
         navigationEnabled: Boolean = false,
         initialPage: QuickEditorPage = QuickEditorPage.AboutEditor,


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR extends the triggers for displaying the Unsaved Changes alert dialog on modal dismissal actions by integrating the new confirmDetentChange callback and refactoring the dismissal event handling.  
- Extended dismissal triggers to include back button, tap outside, and drag down actions.  
- Refactored modal handling by removing custom scrim click and BackHandler, and updated event naming in both the editor and about editor flows.  
- Updated tests and gradle versions to support the new functionality.

### Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModelTest.kt | Added tests for new OnConfirmDismissal event behavior. |
| gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModelTest.kt | Updated tests to reflect unsaved changes dialog renaming and added dismissal ignore action. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/extensions/QuickEditorExtensions.kt | Removed custom BackHandler and refactored to use modalDetents in the bottom sheet configuration. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/bottomsheet/GravatarQuickEditorBottomSheet.kt | Introduced dismissState management with confirmDetentChange and adjusted modal properties for dismissal. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditorViewModel.kt | Integrated OnConfirmDismissal event handling via an actions channel. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/QuickEditor.kt | Updated UI logic to handle confirmDismissal and lifecycle-aware collection of actions. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/GravatarQuickEditorPage.kt | Updated parameters and renamed navigation functions to support the new dismissal flow. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditField.kt | Removed debug log calls to clean up the component. |
| gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt | Renamed unsaved changes dialog events and implemented corresponding action notifications. |
| gradle/libs.versions.toml | Upgraded composablesCore version to support updated UI components. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt:48**
* [nitpick] The functions 'discardUnsavedChangesDialog' and 'dismissUnsavedChangesDialog' have very similar names, which could lead to confusion. Consider renaming them to more clearly distinguish the actions (for example, using names that reference 'KeepEditing' and 'DiscardChanges').
```
private fun discardUnsavedChangesDialog() {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T11:23:11Z
user: login: etoledom
body: It's working great overall!

- Showing the alert on About Editor from all the dismissal actions possible is working smoothly.
- Avatar Picker dismissal continues to work as expected.

🍅 When testing on the Avatar&About scope, I was able to dismiss the Quick Editor with unsaved changes, when dismissing from the Avatar Editor page:


![CleanShot 2025-05-16 at 09 56 50](https://github.com/user-attachments/assets/f14a42cc-8f83-4822-ac13-8fd1d9721249)

We work this out on iOS by being the QuickEditor (parent) who asks the models if there is any unsaved changes, and its also the one who presents the alert:

![CleanShot 2025-05-16 at 10 00 51](https://github.com/user-attachments/assets/859d0573-2f5a-40c1-ad15-2934b02e8864)




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:02:35Z
user: login: AdamGrzybkowski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:13:31Z
user: login: etoledom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:08:09Z
user: login: AdamGrzybkowski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:34:17Z
user: login: etoledom
body: > I would keep the PR as is and extend the alert to the AvatarPicker page separately to align with iOS.

I agree! Let's do that.

Thank you for the update on the names 🙏 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:55:13Z

Review comments:
diff_hunk: @@ -120,28 +120,63 @@ public fun GravatarQuickEditorBottomSheet(
             }
         },
         onDismiss = onDismiss,
-        modalBottomSheetState = rememberGravatarModalBottomSheetState(
-            avatarPickerContentLayout = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout,
-        ),
+        modalDetents = gravatarQuickEditorParams.scopeOption.avatarPickerContentLayout.modalDetents(),
     )
 }
 
+internal enum class DismissConfirmationState {
+    Delegate,
+    Confirm,
+    Delegated,
+}
+
 @Composable
 internal fun GravatarQuickEditorBottomSheet(
     gravatarQuickEditorParams: GravatarQuickEditorParams,
     authenticationMethod: AuthenticationMethod,
     updateHandler: UpdateHandler,
+    modalDetents: ModalDetents,
     onDismiss: (dismissReason: GravatarQuickEditorDismissReason) -> Unit = {},
-    modalBottomSheetState: ModalBottomSheetState,
+    onCurrentDetentChanged: (sheetDetent: SheetDetent) -> Unit = {},
 ) {
     val coroutineScope = rememberCoroutineScope()
+    var dismissState: DismissConfirmationState by remember { mutableStateOf(DismissConfirmationState.Delegate) }
+
+    val onDismissIgnored = {
+        dismissState = DismissConfirmationState.Delegate
+    }
+
+    val modalBottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
+        initialDetent = modalDetents.initialDetent,
+        detents = modalDetents.detents,
+        confirmDetentChange = { sheetDetent ->
+            // We only care about the Hidden state
+            if (sheetDetent == Hidden) {
+                when (dismissState) {
+                    DismissConfirmationState.Confirm -> true
+                    DismissConfirmationState.Delegated -> false
+                    DismissConfirmationState.Delegate -> {
+                        dismissState = DismissConfirmationState.Delegated
+                        false
+                    }
+                }
+            } else {
path: gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/editor/bottomsheet/GravatarQuickEditorBottomSheet.kt
user: login: Copilot
body: Consider adding inline documentation that clarifies the state transitions of dismissState within the confirmDetentChange callback to improve readability and maintainability.
```suggestion
            // Handle state transitions only when the sheet is transitioning to the Hidden state.
            if (sheetDetent == Hidden) {
                when (dismissState) {
                    // If the state is Confirm, allow the transition to Hidden.
                    DismissConfirmationState.Confirm -> true
                    
                    // If the state is Delegated, block the transition to Hidden.
                    DismissConfirmationState.Delegated -> false
                    
                    // If the state is Delegate, transition to Delegated and block the transition to Hidden.
                    DismissConfirmationState.Delegate -> {
                        dismissState = DismissConfirmationState.Delegated
                        false
                    }
                }
            } else {
                // For all other states, allow the transition.
```
created_at: 2025-05-15T11:23:10Z
author_association: NONE
diff_hunk: @@ -40,23 +40,34 @@ internal class AboutEditorViewModel(
             is AboutEditorEvent.OnAboutFieldUpdated -> updateAboutField(aboutEditorEvent.aboutField)
             AboutEditorEvent.OnSaveClicked -> saveProfile()
             AboutEditorEvent.OnDoneClicked -> checkForUnsavedChanges()
-            AboutEditorEvent.OnDiscardDialogDismissed,
-            AboutEditorEvent.OnDiscardConfirmed,
-            -> dismissDiscardChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDismissed -> dismissUnsavedChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDiscarded -> discardUnsavedChangesDialog()
path: gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
user: login: etoledom
body: Nit: `Dismiss` and `Discard` could be confusing to determine the action of each 🤔 

Both `DialogDismissed` - `DialogDiscarded`,
and `dismissUnsavedChanges` - `discardUnsavedChanges` pairs could mean the same to me.

I'd guess that `Dismiss` will only dismiss the alert and `Discard` will discard changes, as I know what's the idea of the alert and the feature.

On iOS we have `keep editing` and `discard changes` (though the internal implementaion is quite different).

But maybe `OnUnsavedChangesDialogDismissed` - `OnUnsavedChangesDialogChangesDiscarded`
and `dismissDialogUnsavedChanges` - `discardUnsavedChangesDialog`

As the dismiss is about the alert and the discard is about the changes 🤔 

created_at: 2025-05-16T07:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,23 +40,34 @@ internal class AboutEditorViewModel(
             is AboutEditorEvent.OnAboutFieldUpdated -> updateAboutField(aboutEditorEvent.aboutField)
             AboutEditorEvent.OnSaveClicked -> saveProfile()
             AboutEditorEvent.OnDoneClicked -> checkForUnsavedChanges()
-            AboutEditorEvent.OnDiscardDialogDismissed,
-            AboutEditorEvent.OnDiscardConfirmed,
-            -> dismissDiscardChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDismissed -> dismissUnsavedChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDiscarded -> discardUnsavedChangesDialog()
path: gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
user: login: AdamGrzybkowski
body: Would you say that `OnUnsavedChangesExitClick` and `OnUnsavedChangesKeepEdittingClick` would be less confusing? 
created_at: 2025-05-16T08:13:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,23 +40,34 @@ internal class AboutEditorViewModel(
             is AboutEditorEvent.OnAboutFieldUpdated -> updateAboutField(aboutEditorEvent.aboutField)
             AboutEditorEvent.OnSaveClicked -> saveProfile()
             AboutEditorEvent.OnDoneClicked -> checkForUnsavedChanges()
-            AboutEditorEvent.OnDiscardDialogDismissed,
-            AboutEditorEvent.OnDiscardConfirmed,
-            -> dismissDiscardChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDismissed -> dismissUnsavedChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDiscarded -> discardUnsavedChangesDialog()
path: gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
user: login: etoledom
body: > Would you say that OnUnsavedChangesExitClick and OnUnsavedChangesKeepEdittingClick would be less confusing?

That's perfectly clear! 🎉 
created_at: 2025-05-16T12:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,23 +40,34 @@ internal class AboutEditorViewModel(
             is AboutEditorEvent.OnAboutFieldUpdated -> updateAboutField(aboutEditorEvent.aboutField)
             AboutEditorEvent.OnSaveClicked -> saveProfile()
             AboutEditorEvent.OnDoneClicked -> checkForUnsavedChanges()
-            AboutEditorEvent.OnDiscardDialogDismissed,
-            AboutEditorEvent.OnDiscardConfirmed,
-            -> dismissDiscardChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDismissed -> dismissUnsavedChangesDialog()
+            AboutEditorEvent.OnUnsavedChangesDialogDiscarded -> discardUnsavedChangesDialog()
path: gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/abouteditor/AboutEditorViewModel.kt
user: login: AdamGrzybkowski
body: Updated!
created_at: 2025-05-16T12:34:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: wpmobilebot
created_at: 2025-05-15T11:26:06Z
body: <!-- REUSE_ID: app-prototype-build-link -->

<details><summary>📲 You can test the changes from this Pull Request in <b>Gravatar Demo</b> by scanning the QR code below to install the corresponding build.</summary>
<table>
<tr>
  <td rowspan='3' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Fcdn.a8c-ci.services%2F428e0f921ef77474515d6e3cdb7d3e0a4f1f62c9%2Fgravatar-demo-prototype-build-pr630-eba852e.apk' width='250' height='250' /></td>
  <td><b>App Name</b></td><td>Gravatar Demo</td>
</tr>
<tr><td><b>Commit</b></td><td>eba852e6b148be2e482a45f914dee0d5f3656dce</td></tr>
<tr><td><b>Direct Download</b></td><td><a href='https://cdn.a8c-ci.services/428e0f921ef77474515d6e3cdb7d3e0a4f1f62c9/gravatar-demo-prototype-build-pr630-eba852e.apk'><code>gravatar-demo-prototype-build-pr630-eba852e.apk</code></a></td></tr>
</table>
</details>

author_association: COLLABORATOR
user: login: wpmobilebot
created_at: 2025-05-15T11:30:29Z
body: <!-- DO NOT REMOVE ci-toolkit-comment-identifier: dependency-diff -->

## Project dependencies changes
Content exceeds 65400 characters. [Navigate to Buildkite build artifacts](https://buildkite.com/automattic/gravatar-sdk-android/builds/1685#0196d936-e6b4-49af-a712-5660b9382d49) to see details.

author_association: COLLABORATOR
user: login: AdamGrzybkowski
created_at: 2025-05-16T08:08:03Z
body: >🍅 When testing on the Avatar&About scope, I was able to dismiss the Quick Editor with unsaved changes, when dismissing from the Avatar Editor page:


That's true. It was like that with the `Done` button as well so I kept the behavior for now. 

>We work this out on iOS by being the QuickEditor (parent) who asks the models if there is any unsaved changes, and its also the one who presents the alert:

I'm worried that showing the same alert while in the Avatar Picker could be confusing. It feels like it needs a better copy to say where the unsaved changes are with a quick action to navigate there. 
author_association: CONTRIBUTOR
user: login: etoledom
created_at: 2025-05-16T12:09:44Z
body: > I'm worried that showing the same alert while in the Avatar Picker could be confusing. It feels like it needs a better copy to say where the unsaved changes are with a quick action to navigate there.

This is true, we can have a wider conversation about this.

I'm still hoping to have some visual cues for unsaved changes somewhere. But we agreed that this is outside of scope for now 🤔 


author_association: CONTRIBUTOR
user: login: AdamGrzybkowski
created_at: 2025-05-16T12:27:08Z
body: >This is true, we can have a wider conversation about this.

I would keep the PR as is and extend the alert to the AvatarPicker page separately to align with iOS.
author_association: CONTRIBUTOR