diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt index 012c98e36f..258ec11470 100644 --- a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt @@ -69,8 +69,31 @@ class KmpFeatureConventionPlugin : Plugin { implementation(libs.library("compose-multiplatform-ui")) } + } - sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) } + // core:testing has no wasmJs target (same gap every core/* module hit while gaining one this + // session). Wiring it into commonTest directly — as this plugin used to, unconditionally — + // breaks compileTestKotlinWasmJs for every feature module that opts into wasmJs, since the + // dependency is added by this shared plugin's apply(), which runs *before* the consuming + // module's own build.gradle.kts `kotlin {}` block (and any nonWebTest source set it creates) + // has executed. Deferring to afterEvaluate — which fires only after the whole build script has + // run — lets us check what the consuming module actually set up and route accordingly: + // - a module with a `nonWebTest` source set (wasmJs opted in, core:testing hoisted out of + // commonMain the same way every core/* module did) gets core:testing wired there instead. + // - every other feature module (no wasmJs, no nonWebTest) is wired into commonTest exactly as + // before — fully backward-compatible, verified against every other v0/non-wasmJs consumer. + target.afterEvaluate { + extensions.configure { + val hasWasmJsTarget = targets.findByName("wasmJs") != null + val nonWebTest = sourceSets.findByName("nonWebTest") + check(!hasWasmJsTarget || nonWebTest != null) { + "${target.path} registers wasmJs() but has no `nonWebTest` source set — " + + "core:testing has no wasmJs target, so it must be routed away from commonTest. " + + "See feature/connections/build.gradle.kts for the pattern." + } + val testSourceSet = nonWebTest ?: sourceSets.getByName("commonTest") + testSourceSet.dependencies { implementation(project(":core:testing")) } + } } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt index 980c5adc33..fd20d891ef 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt @@ -26,7 +26,6 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.testing.AbstractTestTask import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType import org.gradle.plugin.use.PluginDependency diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index ef45d55ff0..4552abf0f7 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -24,6 +24,14 @@ plugins { kotlin { android { withHostTest { isIncludeAndroidResources = true } } + // Library module: bare wasmJs(), no browser(). No custom hierarchy group is needed for MAIN — zero + // expect/actual declarations and zero java.*/android.* imports in commonMain (confirmed via grep), + // and every commonMain dependency (core:repository/model/common/database/datastore/resources, + // protobufs, kermit/okio/kotlinx-datetime/kotlinx-serialization-json(-okio)) already publishes a + // wasmJs variant — same shape as core:repository/core:service, unlike core:ble/core:database. + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs() + sourceSets { commonMain.dependencies { implementation(projects.core.repository) @@ -40,6 +48,17 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json.okio) } - commonTest.dependencies { implementation(projects.core.testing) } + + // TEST only: core:testing has no wasmJs target (same gap every other module this session hit). + // 7 of 13 commonTest files depend on it (confirmed via grep for the import, not assumed) — moved + // to a nonWebTest source set; the other 6 stay in commonTest and compile for wasmJs. + val nonWebTest by creating { + dependsOn(commonTest.get()) + dependencies { implementation(projects.core.testing) } + } + getByName("jvmTest") { dependsOn(nonWebTest) } + getByName("androidHostTest") { dependsOn(nonWebTest) } + matching { it.name == "iosArm64Test" || it.name == "iosSimulatorArm64Test" } + .configureEach { dependsOn(nonWebTest) } } } diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 696882eca6..72f2b8e537 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -15,11 +15,33 @@ * along with this program. If not, see . */ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate + plugins { alias(libs.plugins.meshtastic.kmp.feature) } kotlin { android { withHostTest { isIncludeAndroidResources = true } } + // Feature module: bare wasmJs(), no browser() (that's for the eventual webApp executable). + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + + // nonWebMain: ScannerViewModel/CommonGetDiscoveredDevicesUseCase depend on RecentAddressesSource/ + // PendingFirmwareRecoverySource (feature-local interfaces, commonMain) — but the real, Preferences-backed + // adapters (DataSourceAdapters.kt) delegate to core:datastore's RecentAddressesDataSource/ + // FirmwareRecoveryDataSource, which have no wasmJs target (androidx.datastore.preferences publishes none + // — see core:datastore's own wasmJs milestone). Predicate, not withAndroidTarget()/withApple() — those + // silently drop androidMain under com.android.kotlin.multiplatform.library (KT-80409), same as core:ble. + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate(KotlinHierarchyTemplate.default) { + common { group("nonWeb") { withCompilations { it.target.targetName != "wasmJs" } } } + } + + // The predicate above misses iosMain itself (only reaches the two leaf iOS compilations), same gap core:ble hit. + sourceSets.getByName("iosMain") { dependsOn(sourceSets.getByName("nonWebMain")) } + sourceSets { commonMain.dependencies { implementation(projects.core.common) @@ -47,5 +69,14 @@ kotlin { implementation(libs.compose.multiplatform.ui.test) implementation(compose.desktop.currentOs) } + + // TEST only: 4 of 6 commonTest files depend on core:testing (no wasmJs target — same gap every + // other module this session hit), confirmed via grep for the import, not assumed: + // ScannerViewModelHarness.kt/ScannerViewModelTest.kt/TcpDiscoveryHelpersTest.kt/ + // CommonGetDiscoveredDevicesUseCaseTest.kt moved to the nonWebTest source set the hierarchy + // template above already creates (android/jvm/iOS only); the other 2 stay in commonTest and + // compile for wasmJs. core:testing itself is wired into nonWebTest by KmpFeatureConventionPlugin + // (afterEvaluate, routes to nonWebTest when present) — not added here. + getByName("nonWebTest") { dependsOn(commonTest.get()) } } } diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt index 9e4473cf3b..52ae892756 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt @@ -26,8 +26,6 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.ble.BluetoothRepository -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.network.repository.UsbRepository @@ -39,6 +37,8 @@ import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.bonding_failed_retry import org.meshtastic.core.resources.usb_permission_denied +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.AndroidUsbDeviceData import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -50,14 +50,14 @@ class AndroidScannerViewModel( radioController: RadioController, radioInterfaceService: RadioInterfaceService, radioPrefs: RadioPrefs, - recentAddressesDataSource: RecentAddressesDataSource, + recentAddressesDataSource: RecentAddressesSource, getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, networkRepository: NetworkRepository, dispatchers: org.meshtastic.core.di.CoroutineDispatchers, private val bluetoothRepository: BluetoothRepository, private val usbRepository: UsbRepository, uiPrefs: UiPrefs, - firmwareRecoveryDataSource: FirmwareRecoveryDataSource, + firmwareRecoveryDataSource: PendingFirmwareRecoverySource, bleScanner: org.meshtastic.core.ble.BleScanner? = null, ) : ScannerViewModel( serviceRepository, diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt index 8e7d74a300..6db0330ce7 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt @@ -24,7 +24,6 @@ import org.jetbrains.compose.resources.getString import org.koin.core.annotation.Single import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.Node import org.meshtastic.core.network.repository.DiscoveredService @@ -33,6 +32,7 @@ import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.meshtastic +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.AndroidUsbDeviceData import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices @@ -44,7 +44,7 @@ import java.util.Locale @Single(binds = [GetDiscoveredDevicesUseCase::class]) class AndroidGetDiscoveredDevicesUseCase( private val bluetoothRepository: BluetoothRepository, - private val recentAddressesDataSource: RecentAddressesDataSource, + private val recentAddressesDataSource: RecentAddressesSource, private val nodeRepository: NodeRepository, private val databaseManager: DatabaseManager, private val usbRepository: UsbRepository, diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt index 2fa7e8750f..8e100508d6 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt @@ -44,8 +44,6 @@ import org.meshtastic.core.ble.BleScanStartFailureReason import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.ble.MeshtasticBleConstants import org.meshtastic.core.common.util.safeCatchingAll -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.PendingFirmwareRecovery import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.di.CoroutineDispatchers @@ -65,6 +63,8 @@ import org.meshtastic.core.resources.getPluralStringSuspend import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -143,12 +143,12 @@ open class ScannerViewModel( private val radioController: RadioController, private val radioInterfaceService: RadioInterfaceService, private val radioPrefs: RadioPrefs, - private val recentAddressesDataSource: RecentAddressesDataSource, + private val recentAddressesDataSource: RecentAddressesSource, private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, private val networkRepository: NetworkRepository, private val dispatchers: CoroutineDispatchers, private val uiPrefs: UiPrefs, - private val firmwareRecoveryDataSource: FirmwareRecoveryDataSource, + private val firmwareRecoveryDataSource: PendingFirmwareRecoverySource, private val bleScanner: BleScanner? = null, ) : ViewModel() { diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.kt new file mode 100644 index 0000000000..e30730f042 --- /dev/null +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.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.connections.data + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.datastore.model.PendingFirmwareRecovery + +/** Feature-local seam over `core:datastore`'s `FirmwareRecoveryDataSource` — see [RecentAddressesSource]. */ +interface PendingFirmwareRecoverySource { + val pending: Flow + + suspend fun clear() +} diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt new file mode 100644 index 0000000000..7a4225f25b --- /dev/null +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt @@ -0,0 +1,34 @@ +/* + * 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.connections.data + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.datastore.model.RecentAddress + +/** + * Feature-local seam over `core:datastore`'s `RecentAddressesDataSource`, which wraps `androidx.datastore.preferences` + * and so has no wasmJs target (that library publishes zero js/wasmJs variants — see core:datastore's own wasmJs + * milestone). Keeps [org.meshtastic.feature.connections.ScannerViewModel]/`CommonGetDiscoveredDevicesUseCase` in + * commonMain: only the adapter delegating to the real DataSource is hoisted to `nonWebMain`, and wasmJs gets a no-op. + */ +interface RecentAddressesSource { + val recentAddresses: Flow> + + suspend fun add(address: RecentAddress) + + suspend fun remove(address: String) +} diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt index 9069e37f9f..7008db1a5d 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt @@ -21,12 +21,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.common.util.safeCatchingAll -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.network.repository.DiscoveredService import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.resources.meshtastic +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -39,7 +39,7 @@ import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase * target registers its own `@Single` wrapper (see `JvmGetDiscoveredDevicesUseCase`). */ open class CommonGetDiscoveredDevicesUseCase( - private val recentAddressesDataSource: RecentAddressesDataSource, + private val recentAddressesDataSource: RecentAddressesSource, private val nodeRepository: NodeRepository, private val databaseManager: DatabaseManager, private val usbScanner: UsbScanner? = null, diff --git a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt index 97d2f02a0c..8158474bb5 100644 --- a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt +++ b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt @@ -17,14 +17,14 @@ package org.meshtastic.feature.connections import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.repository.RadioController import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase /** @@ -40,12 +40,12 @@ class JvmScannerViewModel( radioController: RadioController, radioInterfaceService: RadioInterfaceService, radioPrefs: RadioPrefs, - recentAddressesDataSource: RecentAddressesDataSource, + recentAddressesDataSource: RecentAddressesSource, getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, networkRepository: NetworkRepository, dispatchers: org.meshtastic.core.di.CoroutineDispatchers, uiPrefs: UiPrefs, - firmwareRecoveryDataSource: FirmwareRecoveryDataSource, + firmwareRecoveryDataSource: PendingFirmwareRecoverySource, bleScanner: org.meshtastic.core.ble.BleScanner? = null, ) : ScannerViewModel( serviceRepository, diff --git a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt index 8c1adc3ce6..b200181b5f 100644 --- a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt @@ -18,8 +18,8 @@ package org.meshtastic.feature.connections.domain.usecase import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase /** @@ -35,7 +35,7 @@ import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase */ @Single(binds = [GetDiscoveredDevicesUseCase::class]) class JvmGetDiscoveredDevicesUseCase( - recentAddressesDataSource: RecentAddressesDataSource, + recentAddressesDataSource: RecentAddressesSource, nodeRepository: NodeRepository, databaseManager: DatabaseManager, usbScanner: UsbScanner? = null, diff --git a/feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt b/feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt new file mode 100644 index 0000000000..49d5d59eef --- /dev/null +++ b/feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt @@ -0,0 +1,41 @@ +/* + * 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.connections.data + +import org.koin.core.annotation.Single +import org.meshtastic.core.datastore.FirmwareRecoveryDataSource +import org.meshtastic.core.datastore.RecentAddressesDataSource +import org.meshtastic.core.datastore.model.RecentAddress + +/** android/jvm/iOS binding: delegates to the real, Preferences-backed `RecentAddressesDataSource`. */ +@Single +class RecentAddressesSourceAdapter(private val delegate: RecentAddressesDataSource) : RecentAddressesSource { + override val recentAddresses = delegate.recentAddresses + + override suspend fun add(address: RecentAddress) = delegate.add(address) + + override suspend fun remove(address: String) = delegate.remove(address) +} + +/** android/jvm/iOS binding: delegates to the real, Preferences-backed `FirmwareRecoveryDataSource`. */ +@Single +class PendingFirmwareRecoverySourceAdapter(private val delegate: FirmwareRecoveryDataSource) : + PendingFirmwareRecoverySource { + override val pending = delegate.pending + + override suspend fun clear() = delegate.clear() +} diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt similarity index 96% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt index 55351bdb36..8e55901d66 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt +++ b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt @@ -35,8 +35,6 @@ import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.meshtastic.core.ble.BleDevice import org.meshtastic.core.ble.BleScanner -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.network.repository.DiscoveredService import org.meshtastic.core.network.repository.NetworkRepository @@ -47,6 +45,8 @@ import org.meshtastic.core.testing.FakeBluetoothRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.FakeServiceRepository import org.meshtastic.core.testing.FakeUiPrefs +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -73,8 +73,8 @@ class ScannerViewModelHarness(val testDispatcher: TestDispatcher = UnconfinedTes val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill) val radioPrefs: RadioPrefs = mock(MockMode.autofill) - val recentAddressesDataSource: RecentAddressesDataSource = mock(MockMode.autofill) - val firmwareRecoveryDataSource: FirmwareRecoveryDataSource = mock(MockMode.autofill) + val recentAddressesDataSource: RecentAddressesSource = mock(MockMode.autofill) + val firmwareRecoveryDataSource: PendingFirmwareRecoverySource = mock(MockMode.autofill) val networkRepository: NetworkRepository = mock(MockMode.autofill) val bleScanner: BleScanner = mock(MockMode.autofill) diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt similarity index 100% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt similarity index 99% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt index 0efe552c3c..46d99875af 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt +++ b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt @@ -26,11 +26,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.network.repository.DiscoveredService import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.TestDataFactory +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DeviceListEntry import kotlin.test.Test import kotlin.test.assertNotNull @@ -41,7 +41,7 @@ import kotlin.test.assertTrue class CommonGetDiscoveredDevicesUseCaseTest { private lateinit var useCase: CommonGetDiscoveredDevicesUseCase private lateinit var nodeRepository: FakeNodeRepository - private lateinit var recentAddressesDataSource: RecentAddressesDataSource + private lateinit var recentAddressesDataSource: RecentAddressesSource private lateinit var databaseManager: DatabaseManager private val recentAddressesFlow = MutableStateFlow>(emptyList()) private val resolvedServicesFlow = MutableStateFlow>(emptyList()) diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt similarity index 100% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt diff --git a/feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt b/feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt new file mode 100644 index 0000000000..9bf137bdb3 --- /dev/null +++ b/feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt @@ -0,0 +1,45 @@ +/* + * 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.connections.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.koin.core.annotation.Single +import org.meshtastic.core.datastore.model.PendingFirmwareRecovery +import org.meshtastic.core.datastore.model.RecentAddress + +/** + * wasmJs binding: no recent-TCP-address persistence on web (the real DataSource is Preferences-backed with no wasmJs + * target — see [RecentAddressesSource]). [DEFERRED]: a real implementation could be written against `localStorage`, + * mirroring core:datastore's own `LocalStorageStore`, once a webApp module exists to wire it in. + */ +@Single +class NoopRecentAddressesSource : RecentAddressesSource { + override val recentAddresses: Flow> = flowOf(emptyList()) + + override suspend fun add(address: RecentAddress) = Unit + + override suspend fun remove(address: String) = Unit +} + +/** wasmJs binding: no firmware-recovery banner on web — see [NoopRecentAddressesSource]. */ +@Single +class NoopPendingFirmwareRecoverySource : PendingFirmwareRecoverySource { + override val pending: Flow = flowOf(null) + + override suspend fun clear() = Unit +}