fix(ios): build ktor clients on Darwin so wss/https work on a device

CIO has no TLS implementation on kotlin/native, so every secure
connection died at the handshake with "TLS sessions are not supported
on Native platform". Pick the engine per platform -- CIO on android
and the desktop jvm, Darwin on ios -- and pin both the relay socket
client and coil's image fetcher to it.

Coil is the half that is easy to miss. Nothing in the app configured an
ImageLoader, so its default ktor fetcher built a bare `HttpClient()` and
took whichever engine the classpath served first; every avatar and
preview over https would have failed the same way the sockets did, with
nothing in the app's own code to point at. `MantraApp` is the one place
both call sites are reachable from, and the factory is installed above
`MantraTheme` so it is in place before the first AsyncImage composes.

The darwin dependency goes inside the existing isMacOsX gate, so nothing
about resolution on a linux or windows host changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso NgakoandClaude Opus 5 committed 2026-10-02 23:57:59 +02:00
1 parent d024838bae
commit e6c3c8a244
8 files changed
+59 -1

No files matched your search

+6
View File
@@ -153,6 +153,12 @@ kotlin {
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) { if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) {
iosMain.dependencies { iosMain.dependencies {
implementation(libs.sqldelight.native.driver) implementation(libs.sqldelight.native.driver)
// CIO has no TLS implementation on native ("TLS sessions are not
// supported on Native platform"), so every wss/https call it makes on
// a device fails at the handshake. Darwin (NSURLSession) is the engine
// apple targets must use.
implementation(libs.ktor.client.darwin)
} }
} }
} }
@@ -0,0 +1,6 @@
package press.mantra.compose.network
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.cio.CIO
internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = CIO
@@ -5,17 +5,38 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory
import io.ktor.client.HttpClient
import press.mantra.compose.network.platformHttpClientEngineFactory
import press.mantra.compose.ui.composable.navigation.MantraNavHost import press.mantra.compose.ui.composable.navigation.MantraNavHost
import press.mantra.compose.ui.composable.widgets.ProvideSnackbarHost import press.mantra.compose.ui.composable.widgets.ProvideSnackbarHost
import press.mantra.compose.ui.theme.MantraTheme import press.mantra.compose.ui.theme.MantraTheme
import fr.acinq.phoenix.PhoenixGlobal import fr.acinq.phoenix.PhoenixGlobal
/**
* Coil's default ktor fetcher builds a bare `HttpClient()`, whose engine is whichever the
* classpath happens to serve first. With CIO that means no TLS on ios, so images over
* https fail exactly like the relay sockets did. Pin the singleton loader to the same
* platform engine the sockets use.
*/
private val imageHttpClient by lazy { HttpClient(platformHttpClientEngineFactory()) }
@Composable @Composable
fun MantraApp( fun MantraApp(
navController: NavHostController, navController: NavHostController,
mantraGlobal: MantraGlobal, mantraGlobal: MantraGlobal,
phoenixGlobal: PhoenixGlobal phoenixGlobal: PhoenixGlobal
) { ) {
setSingletonImageLoaderFactory { context ->
ImageLoader.Builder(context)
.components {
add(KtorNetworkFetcherFactory(httpClient = { imageHttpClient }))
}
.build()
}
MantraTheme { MantraTheme {
Surface( Surface(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -0,0 +1,11 @@
package press.mantra.compose.network
import io.ktor.client.engine.HttpClientEngineFactory
/**
* The engine every Ktor client in the app is built on. CIO serves android and the desktop
* jvm; on apple targets its TLS layer does not exist (`TLS sessions are not supported on
* Native platform`), so every wss/https call fails at the handshake and ios uses Darwin
* (NSURLSession) instead.
*/
internal expect fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*>
@@ -4,10 +4,11 @@ import io.ktor.client.HttpClient
import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import press.mantra.compose.network.platformHttpClientEngineFactory
internal val defaultSocketsHttpClient by lazy { internal val defaultSocketsHttpClient by lazy {
HttpClient { HttpClient(platformHttpClientEngineFactory()) {
install(WebSockets) { install(WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(Json { contentConverter = KotlinxWebsocketSerializationConverter(Json {
isLenient = true isLenient = true
@@ -0,0 +1,6 @@
package press.mantra.compose.network
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.darwin.Darwin
internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = Darwin
@@ -0,0 +1,6 @@
package press.mantra.compose.network
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.cio.CIO
internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = CIO
+1
View File
@@ -80,6 +80,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" } kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
lightning-kmp-core = { module = "fr.acinq.lightning:lightning-kmp-core", version.ref = "lightningKmpCore" } lightning-kmp-core = { module = "fr.acinq.lightning:lightning-kmp-core", version.ref = "lightningKmpCore" }