From 7d79b9844128dc4ec74624f2367c88ec492c9dec Mon Sep 17 00:00:00 2001
From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Date: Sun, 19 Jul 2026 11:59:02 -0700
Subject: [PATCH 1/2] fix: align channel QR share contract
---
.../meshtastic/core/model/util/ChannelSet.kt | 2 +-
.../core/model/util/ChannelSetUrlTest.kt | 23 ++++++++++++++--
.../settings/radio/channel/ChannelScreen.kt | 4 ++-
.../radio/channel/ChannelScreenTest.kt | 27 +++++++++++++++++++
4 files changed, 52 insertions(+), 4 deletions(-)
create mode 100644 feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt
diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt
index 77730ed7e8..4e8d0891ff 100644
--- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt
+++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt
@@ -184,7 +184,7 @@ private fun ChannelSettings.withoutPositionSharing(): ChannelSettings =
* @param upperCasePrefix portions of the URL can be upper case to make for more efficient QR codes
*/
fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false, shouldAdd: Boolean = false): CommonUri {
- val channelBytes = ChannelSet.ADAPTER.encode(this)
+ val channelBytes = ChannelSet.ADAPTER.encode(if (shouldAdd) copy(lora_config = null) else this)
val enc = channelBytes.toByteString().base64Url().replace("=", "")
val p = if (upperCasePrefix) CHANNEL_URL_PREFIX.uppercase() else CHANNEL_URL_PREFIX
val query = if (shouldAdd) "?add=true" else ""
diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt
index 1229a44bbb..e047d4c2f1 100644
--- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt
+++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt
@@ -16,6 +16,7 @@
*/
package org.meshtastic.core.model.util
+import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.proto.ChannelSet
@@ -43,16 +44,34 @@ class ChannelSetUrlTest {
fun `all supported channel counts preserve settings for replace and add`() {
for (channelCount in 1..8) {
val original = channelSet(channelCount)
- val replace = original.getChannelUrl().toChannelSet()
- val add = original.getChannelUrl(shouldAdd = true).toChannelSet()
+ val replaceUrl = original.getChannelUrl()
+ val addUrl = original.getChannelUrl(shouldAdd = true)
+ val replace = replaceUrl.toChannelSet()
+ val add = addUrl.toChannelSet()
+ val replacePayload = replaceUrl.encodedChannelSet()
+ val addPayload = addUrl.encodedChannelSet()
+ assertEquals(original.settings, replacePayload.settings, "$channelCount-channel replace payload settings")
+ assertEquals(
+ original.lora_config,
+ replacePayload.lora_config,
+ "$channelCount-channel replace payload LoRa config",
+ )
assertEquals(original.settings, replace.settings, "$channelCount-channel replace settings")
assertEquals(original.lora_config, replace.lora_config, "$channelCount-channel replace LoRa config")
+ assertEquals(original.settings, addPayload.settings, "$channelCount-channel add payload settings")
+ assertNull(addPayload.lora_config, "$channelCount-channel add payload must omit LoRa config")
assertEquals(original.settings, add.settings, "$channelCount-channel add settings")
assertNull(add.lora_config, "$channelCount-channel add must not retune")
}
}
+ private fun CommonUri.encodedChannelSet(): ChannelSet {
+ val base64 = requireNotNull(fragment).substringBefore('?').replace('-', '+').replace('_', '/')
+ val bytes = requireNotNull(base64.decodeBase64())
+ return ChannelSet.ADAPTER.decode(bytes)
+ }
+
private fun channelSet(channelCount: Int): ChannelSet = ChannelSet(
settings =
(0 until channelCount).map { index ->
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
index 03b360ba70..b89f0719e1 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
@@ -100,6 +100,8 @@ import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
+internal const val DEFAULT_SHOULD_ADD_CHANNELS = false
+
/**
* Composable screen for managing and sharing Meshtastic channels. Allows users to view, edit, and share channel
* configurations via QR codes or URLs.
@@ -126,7 +128,7 @@ fun ChannelScreen(
var showResetDialog by rememberSaveable { mutableStateOf(false) }
- var shouldAddChannelsState by remember { mutableStateOf(true) }
+ var shouldAddChannelsState by remember { mutableStateOf(DEFAULT_SHOULD_ADD_CHANNELS) }
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt
new file mode 100644
index 0000000000..5d865d38b8
--- /dev/null
+++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.channel
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+
+class ChannelScreenTest {
+ @Test
+ fun `channel sharing defaults to replace`() {
+ assertFalse(DEFAULT_SHOULD_ADD_CHANNELS)
+ }
+}
From 689945c34ed6d9e3453a29dde08e86e99bf32ada Mon Sep 17 00:00:00 2001
From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Date: Sun, 19 Jul 2026 12:09:38 -0700
Subject: [PATCH 2/2] test: bind channel share default to URL flow
---
.../settings/radio/channel/ChannelScreen.kt | 19 ++++-------
.../radio/channel/ChannelShareState.kt | 33 +++++++++++++++++++
.../radio/channel/ChannelScreenTest.kt | 15 +++++++--
3 files changed, 53 insertions(+), 14 deletions(-)
create mode 100644 feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelShareState.kt
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
index b89f0719e1..dfe6f1d70e 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt
@@ -64,7 +64,6 @@ import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.defaultPresetFor
-import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add
@@ -100,8 +99,6 @@ import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
-internal const val DEFAULT_SHOULD_ADD_CHANNELS = false
-
/**
* Composable screen for managing and sharing Meshtastic channels. Allows users to view, edit, and share channel
* configurations via QR codes or URLs.
@@ -128,7 +125,7 @@ fun ChannelScreen(
var showResetDialog by rememberSaveable { mutableStateOf(false) }
- var shouldAddChannelsState by remember { mutableStateOf(DEFAULT_SHOULD_ADD_CHANNELS) }
+ val channelShareState = rememberChannelShareState()
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
@@ -224,8 +221,7 @@ fun ChannelScreen(
if (showShareDialog) {
ChannelShareDialog(
- channelSet = selectedChannelSet,
- shouldAddChannel = shouldAddChannelsState,
+ uriString = channelShareState.uriString(selectedChannelSet),
onDismiss = { showShareDialog = false },
)
}
@@ -266,14 +262,14 @@ fun ChannelScreen(
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
SegmentedButton(
label = { Text(text = stringResource(Res.string.replace)) },
- onClick = { shouldAddChannelsState = false },
- selected = !shouldAddChannelsState,
+ onClick = { channelShareState.shouldAdd = false },
+ selected = !channelShareState.shouldAdd,
shape = SegmentedButtonDefaults.itemShape(0, 2),
)
SegmentedButton(
label = { Text(text = stringResource(Res.string.add)) },
- onClick = { shouldAddChannelsState = true },
- selected = shouldAddChannelsState,
+ onClick = { channelShareState.shouldAdd = true },
+ selected = channelShareState.shouldAdd,
shape = SegmentedButtonDefaults.itemShape(1, 2),
)
}
@@ -305,8 +301,7 @@ fun ChannelScreen(
}
@Composable
-private fun ChannelShareDialog(channelSet: ChannelSet, shouldAddChannel: Boolean, onDismiss: () -> Unit) {
- val uriString = channelSet.getChannelUrl(false, shouldAddChannel).toString()
+private fun ChannelShareDialog(uriString: String, onDismiss: () -> Unit) {
QrDialog(title = stringResource(Res.string.share_channels_qr), uriString = uriString, onDismiss = onDismiss)
}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelShareState.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelShareState.kt
new file mode 100644
index 0000000000..b9bd23dd2a
--- /dev/null
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelShareState.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.channel
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import org.meshtastic.core.model.util.getChannelUrl
+import org.meshtastic.proto.ChannelSet
+
+internal class ChannelShareState {
+ var shouldAdd by mutableStateOf(false)
+
+ fun uriString(channelSet: ChannelSet): String = channelSet.getChannelUrl(shouldAdd = shouldAdd).toString()
+}
+
+@Composable internal fun rememberChannelShareState(): ChannelShareState = remember { ChannelShareState() }
diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt
index 5d865d38b8..6425fe9c21 100644
--- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt
+++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt
@@ -16,12 +16,23 @@
*/
package org.meshtastic.feature.settings.radio.channel
+import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.model.util.toChannelSet
+import org.meshtastic.proto.ChannelSet
+import org.meshtastic.proto.Config.LoRaConfig
+import org.meshtastic.proto.Config.LoRaConfig.RegionCode
import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFalse
class ChannelScreenTest {
@Test
- fun `channel sharing defaults to replace`() {
- assertFalse(DEFAULT_SHOULD_ADD_CHANNELS)
+ fun `channel share state defaults to a replace URL`() {
+ val channelSet = ChannelSet(lora_config = LoRaConfig(region = RegionCode.US))
+
+ val url = ChannelShareState().uriString(channelSet)
+
+ assertFalse(url.contains("?add=true"))
+ assertEquals(channelSet.lora_config, CommonUri.parse(url).toChannelSet().lora_config)
}
}