Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .skills/compose-ui/strings-index.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,19 @@
<string name="override_duty_cycle">Override Duty Cycle</string>
<string name="override_frequency_mhz">Frequency Override</string>
<string name="pa_fan_disabled">PA fan disabled</string>
<!-- PACKET -->
<string name="packet_authenticity">Packet authenticity</string>
<string name="packet_authenticity_balanced">Balanced — Prefer authenticated</string>
<string name="packet_authenticity_balanced_summary">Recommended. Reject unsigned downgrade attempts from nodes known to sign.</string>
<string name="packet_authenticity_compatible">Compatible — Accept unsigned</string>
<string name="packet_authenticity_compatible_summary">Authenticate packets when possible, but accept unsigned traffic for maximum compatibility.</string>
<string name="packet_authenticity_level">Protection level</string>
<string name="packet_authenticity_strict">Strict — Require authentication</string>
<string name="packet_authenticity_strict_confirm">Enable Strict</string>
<string name="packet_authenticity_strict_confirmation">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.</string>
<string name="packet_authenticity_strict_summary">Only show and process cryptographically authenticated mesh packets. Older nodes and oversized packets may disappear.</string>
<string name="packet_authenticity_strict_title">Enable Strict authentication?</string>
<string name="packet_authenticity_unsupported">This connected device does not support packet signature verification.</string>
<string name="pairing_mode">Pairing mode</string>
<string name="password">Password</string>
<!-- PAX -->
Expand Down Expand Up @@ -1836,4 +1849,3 @@
<string name="zh_CN" translatable="false">简体中文</string>
<string name="zh_TW" translatable="false">繁體中文</string>
</resources>

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,6 +90,8 @@ data class DropDownItem<T>(
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")
Expand Down Expand Up @@ -177,6 +180,7 @@ fun <T> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
@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 = {})
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading