diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index 2e02059559..d749810954 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -1217,6 +1217,19 @@ override_console_serial_port
override_duty_cycle
override_frequency_mhz
pa_fan_disabled
+### PACKET ###
+packet_authenticity
+packet_authenticity_balanced
+packet_authenticity_balanced_summary
+packet_authenticity_compatible
+packet_authenticity_compatible_summary
+packet_authenticity_level
+packet_authenticity_strict
+packet_authenticity_strict_confirm
+packet_authenticity_strict_confirmation
+packet_authenticity_strict_summary
+packet_authenticity_strict_title
+packet_authenticity_unsupported
pairing_mode
password
### PAX ###
diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index 252be871cb..62571fc790 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -1247,6 +1247,19 @@
Override Duty Cycle
Frequency Override
PA fan disabled
+
+ Packet authenticity
+ Balanced — Prefer authenticated
+ Recommended. Reject unsigned downgrade attempts from nodes known to sign.
+ Compatible — Accept unsigned
+ Authenticate packets when possible, but accept unsigned traffic for maximum compatibility.
+ Protection level
+ Strict — Require authentication
+ Enable Strict
+ Strict ignores every remote mesh packet that is not cryptographically authenticated. Older firmware, licensed or ham nodes without PKI keys, and oversized packets may disappear. PKI-authenticated direct messages remain available.
+ Only show and process cryptographically authenticated mesh packets. Older nodes and oversized packets may disappear.
+ Enable Strict authentication?
+ This connected device does not support packet signature verification.
Pairing mode
Password
@@ -1836,4 +1849,3 @@
简体中文
繁體中文
-
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
index 6cec427ce0..b518cdd77b 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
@@ -89,6 +90,8 @@ data class DropDownItem(
val color: Color? = null,
/** When false, the item is shown greyed-out and cannot be selected. */
val enabled: Boolean = true,
+ /** Optional stable semantics tag for automated tests. */
+ val testTag: String? = null,
)
@JvmName("DropDownPreferencePairs")
@@ -177,6 +180,7 @@ fun DropDownPreference(
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items.forEach { selectionOption ->
DropdownMenuItem(
+ modifier = selectionOption.testTag?.let { Modifier.testTag(it) } ?: Modifier,
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
if (selectionOption.icon != null) {
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt
new file mode 100644
index 0000000000..578eee42be
--- /dev/null
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 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 .
+ */
+@file:Suppress("PreviewPublic")
+
+package org.meshtastic.feature.settings.radio.component
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.proto.Config
+
+@PreviewLightDark
+@Composable
+fun PacketAuthenticityDefaultPreview() {
+ PacketAuthenticitySettingPreview(Config.SecurityConfig().packet_signature_policy)
+}
+
+@PreviewLightDark
+@Composable
+fun PacketAuthenticityBalancedPreview() {
+ PacketAuthenticitySettingPreview(PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED)
+}
+
+@PreviewLightDark
+@Composable
+fun PacketAuthenticityStrictPreview() {
+ PacketAuthenticitySettingPreview(PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT)
+}
+
+@PreviewLightDark
+@Composable
+fun PacketAuthenticityUnsupportedPreview() {
+ AppTheme {
+ Surface(modifier = Modifier.padding(16.dp)) {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = true,
+ supported = false,
+ onPolicyChange = {},
+ )
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+fun PacketAuthenticityStrictConfirmationPreview() {
+ AppTheme { PacketAuthenticityStrictConfirmationDialog(show = true, onConfirm = {}, onDismiss = {}) }
+}
+
+@Composable
+private fun PacketAuthenticitySettingPreview(policy: PacketSignaturePolicy) {
+ AppTheme {
+ Surface(modifier = Modifier.padding(16.dp)) {
+ PacketAuthenticitySetting(selectedPolicy = policy, connected = true, supported = true, onPolicyChange = {})
+ }
+ }
+}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySetting.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySetting.kt
new file mode 100644
index 0000000000..0efe393eb2
--- /dev/null
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySetting.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 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.feature.settings.radio.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.testTag
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.semantics
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.packet_authenticity
+import org.meshtastic.core.resources.packet_authenticity_balanced
+import org.meshtastic.core.resources.packet_authenticity_balanced_summary
+import org.meshtastic.core.resources.packet_authenticity_compatible
+import org.meshtastic.core.resources.packet_authenticity_compatible_summary
+import org.meshtastic.core.resources.packet_authenticity_level
+import org.meshtastic.core.resources.packet_authenticity_strict
+import org.meshtastic.core.resources.packet_authenticity_strict_confirm
+import org.meshtastic.core.resources.packet_authenticity_strict_confirmation
+import org.meshtastic.core.resources.packet_authenticity_strict_summary
+import org.meshtastic.core.resources.packet_authenticity_strict_title
+import org.meshtastic.core.resources.packet_authenticity_unsupported
+import org.meshtastic.core.ui.component.DropDownItem
+import org.meshtastic.core.ui.component.DropDownPreference
+import org.meshtastic.core.ui.component.MeshtasticResourceDialog
+import org.meshtastic.core.ui.component.TitledCard
+import org.meshtastic.proto.Config
+
+internal typealias PacketSignaturePolicy = Config.SecurityConfig.PacketSignaturePolicy
+
+internal const val PACKET_AUTHENTICITY_SELECTOR_TEST_TAG = "packet_authenticity_selector"
+internal const val PACKET_AUTHENTICITY_STRICT_POLICY_TEST_TAG = "packet_authenticity_policy_strict"
+
+@Composable
+internal fun PacketAuthenticitySetting(
+ selectedPolicy: PacketSignaturePolicy,
+ connected: Boolean,
+ supported: Boolean?,
+ onPolicyChange: (PacketSignaturePolicy) -> Unit,
+) {
+ var showStrictConfirmation by rememberSaveable { mutableStateOf(false) }
+ val canConfigurePolicy = connected && supported == true
+
+ LaunchedEffect(canConfigurePolicy) {
+ if (!canConfigurePolicy) {
+ showStrictConfirmation = false
+ }
+ }
+
+ PacketAuthenticityStrictConfirmationDialog(
+ show = showStrictConfirmation,
+ onConfirm = {
+ showStrictConfirmation = false
+ if (canConfigurePolicy) {
+ onPolicyChange(PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT)
+ }
+ },
+ onDismiss = { showStrictConfirmation = false },
+ )
+
+ val items =
+ packetSignaturePolicies.map { policy ->
+ DropDownItem(value = policy, label = stringResource(policy.labelResource()), testTag = policy.testTag())
+ }
+ val summaryResource =
+ if (supported == false) {
+ Res.string.packet_authenticity_unsupported
+ } else {
+ selectedPolicy.summaryResource()
+ }
+
+ TitledCard(title = stringResource(Res.string.packet_authenticity)) {
+ DropDownPreference(
+ title = stringResource(Res.string.packet_authenticity_level),
+ enabled = canConfigurePolicy,
+ items = items,
+ selectedItem = selectedPolicy,
+ modifier =
+ Modifier.testTag(PACKET_AUTHENTICITY_SELECTOR_TEST_TAG).semantics {
+ if (!canConfigurePolicy) disabled()
+ },
+ summary = stringResource(summaryResource),
+ onItemSelected = { policy ->
+ if (
+ policy == PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT &&
+ selectedPolicy != PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT
+ ) {
+ showStrictConfirmation = true
+ } else {
+ onPolicyChange(policy)
+ }
+ },
+ )
+ }
+}
+
+@Composable
+internal fun PacketAuthenticityStrictConfirmationDialog(show: Boolean, onConfirm: () -> Unit, onDismiss: () -> Unit) {
+ if (show) {
+ MeshtasticResourceDialog(
+ titleRes = Res.string.packet_authenticity_strict_title,
+ messageRes = Res.string.packet_authenticity_strict_confirmation,
+ confirmTextRes = Res.string.packet_authenticity_strict_confirm,
+ onConfirm = onConfirm,
+ onDismiss = onDismiss,
+ )
+ }
+}
+
+private val packetSignaturePolicies =
+ listOf(
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_COMPATIBLE,
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT,
+ )
+
+private fun PacketSignaturePolicy.labelResource(): StringResource = when (this) {
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_COMPATIBLE -> Res.string.packet_authenticity_compatible
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED -> Res.string.packet_authenticity_balanced
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT -> Res.string.packet_authenticity_strict
+}
+
+private fun PacketSignaturePolicy.summaryResource(): StringResource = when (this) {
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_COMPATIBLE -> Res.string.packet_authenticity_compatible_summary
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED -> Res.string.packet_authenticity_balanced_summary
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT -> Res.string.packet_authenticity_strict_summary
+}
+
+private fun PacketSignaturePolicy.testTag(): String? = when (this) {
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT -> PACKET_AUTHENTICITY_STRICT_POLICY_TEST_TAG
+ else -> null
+}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt
index fe75ec7cec..2e2731ac4e 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt
@@ -119,6 +119,14 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un
viewModel.setConfig(config)
},
) {
+ item {
+ PacketAuthenticitySetting(
+ selectedPolicy = formState.value.packet_signature_policy,
+ connected = state.connected,
+ supported = state.metadata?.has_xeddsa,
+ onPolicyChange = { policy -> formState.value = formState.value.copy(packet_signature_policy = policy) },
+ )
+ }
item {
TitledCard(title = stringResource(Res.string.direct_message_key)) {
EditBase64Preference(
diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt
new file mode 100644
index 0000000000..d16f9e3d48
--- /dev/null
+++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 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.feature.settings.radio.component
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.v2.runComposeUiTest
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.cancel
+import org.meshtastic.core.resources.getString
+import org.meshtastic.core.resources.packet_authenticity_balanced
+import org.meshtastic.core.resources.packet_authenticity_balanced_summary
+import org.meshtastic.core.resources.packet_authenticity_compatible
+import org.meshtastic.core.resources.packet_authenticity_strict
+import org.meshtastic.core.resources.packet_authenticity_strict_confirm
+import org.meshtastic.core.resources.packet_authenticity_strict_summary
+import org.meshtastic.core.resources.packet_authenticity_unsupported
+import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.proto.Config
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+@OptIn(ExperimentalTestApi::class)
+class PacketAuthenticitySettingTest {
+ @Test
+ fun `protobuf default is compatible`() {
+ assertEquals(
+ PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_COMPATIBLE,
+ Config.SecurityConfig().packet_signature_policy,
+ )
+ }
+
+ @Test
+ fun `balanced selection updates the device config field`() = runComposeUiTest {
+ var updatedConfig = Config.SecurityConfig()
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = updatedConfig.packet_signature_policy,
+ connected = true,
+ supported = true,
+ onPolicyChange = { policy -> updatedConfig = updatedConfig.copy(packet_signature_policy = policy) },
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_compatible)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+
+ assertEquals(PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED, updatedConfig.packet_signature_policy)
+ }
+
+ @Test
+ fun `strict selection requires confirmation`() = runComposeUiTest {
+ var selectedPolicy: PacketSignaturePolicy? = null
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = true,
+ supported = true,
+ onPolicyChange = { selectedPolicy = it },
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict)).performClick()
+
+ assertNull(selectedPolicy)
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertIsDisplayed()
+ onNodeWithText(getString(Res.string.cancel)).performClick()
+ assertNull(selectedPolicy)
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertDoesNotExist()
+ }
+
+ @Test
+ fun `confirmed strict selection updates policy`() = runComposeUiTest {
+ var selectedPolicy: PacketSignaturePolicy? = null
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = true,
+ supported = true,
+ onPolicyChange = { selectedPolicy = it },
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).performClick()
+
+ assertEquals(PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT, selectedPolicy)
+ }
+
+ @Test
+ fun `support loss dismisses strict confirmation without changing policy`() = runComposeUiTest {
+ var supported by mutableStateOf(true)
+ var selectedPolicy: PacketSignaturePolicy? = null
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = true,
+ supported = supported,
+ onPolicyChange = { selectedPolicy = it },
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertIsDisplayed()
+
+ supported = false
+ waitForIdle()
+
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertDoesNotExist()
+ assertNull(selectedPolicy)
+ }
+
+ @Test
+ fun `connection loss dismisses strict confirmation without changing policy`() = runComposeUiTest {
+ var connected by mutableStateOf(true)
+ var selectedPolicy: PacketSignaturePolicy? = null
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = connected,
+ supported = true,
+ onPolicyChange = { selectedPolicy = it },
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertIsDisplayed()
+
+ connected = false
+ waitForIdle()
+
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertDoesNotExist()
+ assertNull(selectedPolicy)
+ }
+
+ @Test
+ fun `selecting current strict policy does not prompt again`() = runComposeUiTest {
+ var selectedPolicy: PacketSignaturePolicy? = null
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT,
+ connected = true,
+ supported = true,
+ onPolicyChange = { selectedPolicy = it },
+ )
+ }
+ }
+
+ val strictLabel = getString(Res.string.packet_authenticity_strict)
+ onNodeWithText(strictLabel).performClick()
+ onNodeWithTag(PACKET_AUTHENTICITY_STRICT_POLICY_TEST_TAG).performClick()
+
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_confirm)).assertDoesNotExist()
+ assertEquals(PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT, selectedPolicy)
+ }
+
+ @Test
+ fun `unsupported firmware cannot open selector`() = runComposeUiTest {
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = true,
+ supported = false,
+ onPolicyChange = {},
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_unsupported)).assertIsDisplayed()
+ onNodeWithTag(PACKET_AUTHENTICITY_SELECTOR_TEST_TAG).assertIsNotEnabled()
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict)).assertDoesNotExist()
+ }
+
+ @Test
+ fun `unknown capability keeps current policy summary while disabled`() = runComposeUiTest {
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = true,
+ supported = null,
+ onPolicyChange = {},
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced_summary)).assertIsDisplayed()
+ onNodeWithText(getString(Res.string.packet_authenticity_unsupported)).assertDoesNotExist()
+ onNodeWithTag(PACKET_AUTHENTICITY_SELECTOR_TEST_TAG).assertIsNotEnabled()
+ }
+
+ @Test
+ fun `disconnected supported device cannot open selector`() = runComposeUiTest {
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_BALANCED,
+ connected = false,
+ supported = true,
+ onPolicyChange = {},
+ )
+ }
+ }
+
+ onNodeWithTag(PACKET_AUTHENTICITY_SELECTOR_TEST_TAG).assertIsNotEnabled()
+ onNodeWithText(getString(Res.string.packet_authenticity_balanced)).performClick()
+ onNodeWithText(getString(Res.string.packet_authenticity_strict)).assertDoesNotExist()
+ }
+
+ @Test
+ fun `strict summary covers all authenticated mesh packets`() = runComposeUiTest {
+ setContent {
+ AppTheme {
+ PacketAuthenticitySetting(
+ selectedPolicy = PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_STRICT,
+ connected = true,
+ supported = true,
+ onPolicyChange = {},
+ )
+ }
+ }
+
+ onNodeWithText(getString(Res.string.packet_authenticity_strict_summary)).assertIsDisplayed()
+ }
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt
index 35721e061d..efef7740fe 100644
--- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt
@@ -36,6 +36,11 @@ import org.meshtastic.feature.settings.component.SampleNodeCompleteFahrenheitPre
import org.meshtastic.feature.settings.component.SampleNodeCompleteImperialPreview
import org.meshtastic.feature.settings.component.SampleNodeCompletePreview
import org.meshtastic.feature.settings.component.SampleNodeCompleteToggleMatrixPreview
+import org.meshtastic.feature.settings.radio.component.PacketAuthenticityBalancedPreview
+import org.meshtastic.feature.settings.radio.component.PacketAuthenticityDefaultPreview
+import org.meshtastic.feature.settings.radio.component.PacketAuthenticityStrictConfirmationPreview
+import org.meshtastic.feature.settings.radio.component.PacketAuthenticityStrictPreview
+import org.meshtastic.feature.settings.radio.component.PacketAuthenticityUnsupportedPreview
import org.meshtastic.feature.settings.radio.component.TakConfigCardPreview
import org.meshtastic.feature.settings.radio.component.TakServerSectionDisabledPreview
import org.meshtastic.feature.settings.radio.component.TakServerSectionEnabledPreview
@@ -113,6 +118,41 @@ fun ScreenshotTakTestCardResults() {
TakTestCardResultsPreview()
}
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPacketAuthenticityDefault() {
+ PacketAuthenticityDefaultPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPacketAuthenticityBalanced() {
+ PacketAuthenticityBalancedPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPacketAuthenticityStrict() {
+ PacketAuthenticityStrictPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPacketAuthenticityUnsupported() {
+ PacketAuthenticityUnsupportedPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPacketAuthenticityStrictConfirmation() {
+ PacketAuthenticityStrictConfirmationPreview()
+}
+
// ---------------------------------------------------------------------------
// Node layout settings screenshots
// ---------------------------------------------------------------------------
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityBalanced_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityBalanced_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..02881d2550
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityBalanced_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityBalanced_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityBalanced_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..73d6fcec99
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityBalanced_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..92c9412c83
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..9de9164ed3
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrictConfirmation_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrictConfirmation_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..1924b18c00
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrictConfirmation_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrictConfirmation_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrictConfirmation_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..596c5c6663
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrictConfirmation_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrict_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrict_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..3122b9bd92
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrict_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrict_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrict_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..071f8380ed
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityStrict_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityUnsupported_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityUnsupported_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..2c3d0c088f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityUnsupported_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityUnsupported_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityUnsupported_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..a0199d7ef9
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityUnsupported_Light_b29dc7a7_0.png differ