diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index fe5f0b808a..edfedf8b6c 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -182,6 +182,8 @@
Accept
Cancel
Discard
+ You have unsaved changes. Are you sure you want to discard them?
+ Stay
Save
New Channel URL received
Report
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/UnsavedChangesDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/UnsavedChangesDialog.kt
new file mode 100644
index 0000000000..5da2f26f2b
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/UnsavedChangesDialog.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.core.ui.component
+
+import androidx.compose.runtime.Composable
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.cancel
+import org.meshtastic.core.resources.discard_changes
+import org.meshtastic.core.resources.stay
+import org.meshtastic.core.resources.unsaved_changes_message
+
+@Composable
+fun UnsavedChangesDialog(onDiscard: () -> Unit, onStay: () -> Unit) {
+ MeshtasticDialog(
+ titleRes = Res.string.cancel,
+ messageRes = Res.string.unsaved_changes_message,
+ confirmTextRes = Res.string.discard_changes,
+ onConfirm = onDiscard,
+ dismissTextRes = Res.string.stay,
+ onDismiss = onStay,
+ )
+}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
index 885e642192..4027222c5f 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
@@ -137,6 +137,27 @@ private fun ChannelConfigScreen(
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
var showChannelLegendDialog by rememberSaveable { mutableStateOf(false) }
+ var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
+
+ val handleBack = {
+ if (isEditing) {
+ showExitConfirmation = true
+ } else {
+ onBack()
+ }
+ }
+
+ org.meshtastic.core.ui.util.PlatformBackHandler(enabled = isEditing) { showExitConfirmation = true }
+
+ if (showExitConfirmation) {
+ org.meshtastic.core.ui.component.UnsavedChangesDialog(
+ onDiscard = {
+ showExitConfirmation = false
+ onBack()
+ },
+ onStay = { showExitConfirmation = false },
+ )
+ }
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
@@ -164,7 +185,7 @@ private fun ChannelConfigScreen(
MainAppBar(
title = title,
canNavigateUp = true,
- onNavigateUp = onBack,
+ onNavigateUp = handleBack,
ourNode = null,
showNodeChip = false,
actions = {},
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
index d1301002aa..9c26520949 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
@@ -31,6 +31,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
@@ -41,6 +45,8 @@ import org.meshtastic.core.resources.discard_changes
import org.meshtastic.core.resources.save_changes
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.PreferenceFooter
+import org.meshtastic.core.ui.component.UnsavedChangesDialog
+import org.meshtastic.core.ui.util.PlatformBackHandler
import org.meshtastic.feature.settings.radio.ResponseState
@Suppress("LongMethod")
@@ -60,14 +66,38 @@ fun > RadioConfigScreenList(
content: LazyListScope.() -> Unit,
) {
val focusManager = LocalFocusManager.current
+ val isEditing = configState.isDirty || additionalDirtyCheck()
+ var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
+
+ val handleBack = {
+ if (isEditing) {
+ showExitConfirmation = true
+ } else {
+ onBack()
+ }
+ }
+
+ PlatformBackHandler(enabled = isEditing) { showExitConfirmation = true }
Box(modifier = modifier) {
+ if (showExitConfirmation) {
+ UnsavedChangesDialog(
+ onDiscard = {
+ showExitConfirmation = false
+ configState.reset()
+ onDiscard()
+ onBack()
+ },
+ onStay = { showExitConfirmation = false },
+ )
+ }
+
Scaffold(
topBar = {
MainAppBar(
title = title,
canNavigateUp = true,
- onNavigateUp = onBack,
+ onNavigateUp = handleBack,
ourNode = null,
showNodeChip = false,
actions = actions,
@@ -75,7 +105,7 @@ fun > RadioConfigScreenList(
)
},
) { innerPadding ->
- val showFooterButtons = configState.isDirty || additionalDirtyCheck()
+ val showFooterButtons = isEditing
LazyColumn(
modifier = Modifier.padding(innerPadding).fillMaxSize(),