From 75b380afeefa9628c5a16b6885a4549c895fc1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sat, 14 Feb 2026 15:45:55 +0300 Subject: [PATCH 01/10] Change approach to publish gradle plugin --- .github/workflows/publish.yml | 2 +- gradle-plugin/build.gradle.kts | 20 +++-------- .../main/kotlin/publish-convention.gradle.kts | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a538e7d7..d2480c8c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish Library to Maven Central +name: Publish Artifacts to Maven Central on: push: diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index daebde44..39f501ce 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -1,19 +1,13 @@ -import java.util.Properties - plugins { kotlin("jvm") id("java-gradle-plugin") id("maven-publish") + alias(libs.plugins.publish.plugin) + id("publish-convention") } group = "ru.bartwell.kick" - -val versionProperties = Properties().apply { - file("${rootProject.projectDir}/version.properties").inputStream().use { load(it) } -} -val pluginVersion: String = versionProperties["libraryVersionName"]?.toString() ?: "1.0.0" - -version = pluginVersion +version = extra["libraryVersionName"] as String gradlePlugin { plugins { @@ -41,13 +35,7 @@ dependencies { } tasks.jar { - manifest.attributes["Implementation-Version"] = pluginVersion -} - -publishing { - repositories { - mavenLocal() - } + manifest.attributes["Implementation-Version"] = version } tasks.test { diff --git a/maven-publishing/src/main/kotlin/publish-convention.gradle.kts b/maven-publishing/src/main/kotlin/publish-convention.gradle.kts index 4d7b20c0..f267215f 100644 --- a/maven-publishing/src/main/kotlin/publish-convention.gradle.kts +++ b/maven-publishing/src/main/kotlin/publish-convention.gradle.kts @@ -1,3 +1,4 @@ +import com.vanniktech.maven.publish.GradlePlugin import com.vanniktech.maven.publish.JavadocJar import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.MavenPublishBaseExtension @@ -8,19 +9,33 @@ configure { coordinates("ru.bartwell.kick", project.name, extra["libraryVersionName"] as String) - configure( - KotlinMultiplatform( - javadocJar = JavadocJar.Empty(), - sourcesJar = true, - androidVariantsToPublish = listOf("release") + if (project.plugins.hasPlugin("java-gradle-plugin")) { + configure( + GradlePlugin( + javadocJar = JavadocJar.Empty(), + sourcesJar = true + ) ) - ) + } else { + configure( + KotlinMultiplatform( + javadocJar = JavadocJar.Empty(), + sourcesJar = true, + androidVariantsToPublish = listOf("release") + ) + ) + } + val isGradlePlugin = project.plugins.hasPlugin("java-gradle-plugin") pom { - name = "Kick" - description = "Kick: Kotlin Inspection & Control Kit. " + - "A modular Compose Multiplatform toolkit for unified in-app " + - "inspection and control of logs, network, databases and more." + name = if (isGradlePlugin) "Kick Gradle Plugin" else "Kick" + description = if (isGradlePlugin) { + "Kick Gradle plugin for Kotlin Multiplatform" + } else { + "Kick: Kotlin Inspection & Control Kit. " + + "A modular Compose Multiplatform toolkit for unified in-app " + + "inspection and control of logs, network, databases and more." + } inceptionYear = "2025" url = "https://github.com/bartwell/kick" licenses { From 7ba9d8ceaf4595dbb4b5bad78497d54c1c71e6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sat, 14 Feb 2026 16:14:18 +0300 Subject: [PATCH 02/10] Update examples in wizard --- content/wizard/app.js | 29 ++++++++++++++++++++++------- content/wizard/app/output.js | 4 +--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/content/wizard/app.js b/content/wizard/app.js index 4aef4deb..76657597 100644 --- a/content/wizard/app.js +++ b/content/wizard/app.js @@ -515,9 +515,7 @@ function getGlueGuideText(item) { function buildKtor3ExampleSnippet() { return [ "val httpClient = HttpClient {", - " install(KickKtor3Plugin) {", - " maxBodySizeBytes = 1024 * 1024L", - " }", + " install(KickKtor3Plugin)", "}", ].join("\n"); } @@ -542,10 +540,19 @@ function buildFirebaseCloudMessagingExampleSnippet(item) { if (item.includeIos) { parts.push( [ - "// Shared Kotlin bridge for iOS push callbacks", - "object IosPushBridge {", - " fun onApnsPayload(userInfo: Map) {", - " Kick.firebaseCloudMessaging.handleApnsPayload(userInfo)", + "// iOS (Swift)", + "import UIKit", + "import shared", + "", + "class AppDelegate: UIResponder, UIApplicationDelegate {", + " func application(", + " _ application: UIApplication,", + " didReceiveRemoteNotification userInfo: [AnyHashable: Any],", + " fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void", + " ) {", + " // your app logic...", + " KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)", + " completionHandler(.noData)", " }", "}", ].join("\n") @@ -593,6 +600,14 @@ function buildControlPanelExampleSnippet() { "if (Kick.controlPanel.getBoolean(\"enableSomeRequest\")) {", " makeRequest(Kick.controlPanel.getString(\"someRequestUrl\"))", "}", + "", + "appScope.launch {", + " Kick.controlPanel.events.collect { event ->", + " if (event is ControlPanelEvent.ButtonClicked && event.id == \"refresh_cache\") {", + " refreshCache()", + " }", + " }", + "}", ].join("\n"); } diff --git a/content/wizard/app/output.js b/content/wizard/app/output.js index fdc9c4f6..0bef5984 100644 --- a/content/wizard/app/output.js +++ b/content/wizard/app/output.js @@ -278,9 +278,7 @@ function buildCommonSnippet(state, selectedModules, hasPlatformBridge) { imports.add("ru.bartwell.kick.module.ktor3.Ktor3Module"); moduleLines.push(" module(Ktor3Module(context))"); moduleLines.push(" // Ktor client integration (outside Kick.init):"); - moduleLines.push(" // install(KickKtor3Plugin) {"); - moduleLines.push(" // maxBodySizeBytes = 1024 * 1024L"); - moduleLines.push(" // }"); + moduleLines.push(" // install(KickKtor3Plugin)"); return; } From d86b7462aaca2e56781b7b23dab5063eb999045c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sat, 14 Feb 2026 20:39:58 +0300 Subject: [PATCH 03/10] Replace enum-based module configuration with type-safe builder methods --- README.md | 8 ++- content/docs/Advanced.md | 17 +++++- content/wizard/app/output.js | 35 +++++------ content/wizard/data/modules.json | 48 +++++++-------- .../kotlin/org/gradle/kotlin/dsl/Project.kt | 6 +- .../ru/bartwell/kick/gradle/KickExtension.kt | 22 +++++-- .../ru/bartwell/kick/gradle/KickModule.kt | 18 +++--- .../bartwell/kick/gradle/KickModulesScope.kt | 25 ++++++++ .../bartwell/kick/gradle/ModuleArtifacts.kt | 32 ++++++---- .../bartwell/kick/gradle/KickExtensionTest.kt | 60 +++++++++---------- sample/plugin-sample/build.gradle.kts | 6 +- 11 files changed, 168 insertions(+), 109 deletions(-) create mode 100644 gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt diff --git a/README.md b/README.md index 948dc58d..fdc1caed 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,10 @@ plugins { } kick { - enabled = KickEnabled.Auto - modules(KickModule.FileExplorer) + enabledAuto() + modules { + fileExplorer() + } } ``` @@ -79,7 +81,7 @@ Kick.init(context) { enableKick(false) ``` -`-Pkick.enabled=true|false` has highest priority and overrides both `enableKick(...)` and `kick { enabled = ... }`. +`-Pkick.enabled=true|false` has highest priority and overrides both `enableKick(...)` and `kick { enabledAuto() / enabled() / disabled() }`. ### Wizard diff --git a/content/docs/Advanced.md b/content/docs/Advanced.md index 1a9a77f8..fded0546 100644 --- a/content/docs/Advanced.md +++ b/content/docs/Advanced.md @@ -30,8 +30,21 @@ plugins { } kick { - enabled = KickEnabled.Auto - modules(KickModule.FileExplorer, KickModule.Ktor3) + enabledAuto() + modules { + controlPanel() + fileExplorer() + firebaseAnalytics() + firebaseCloudMessaging() + ktor3() + layout() + logging() + multiplatformSettings() + overlay() + room() + runner() + sqldelight() + } } // Optional: enableKick(false) or -Pkick.enabled=true|false for override ``` diff --git a/content/wizard/app/output.js b/content/wizard/app/output.js index 0bef5984..ec6798b5 100644 --- a/content/wizard/app/output.js +++ b/content/wizard/app/output.js @@ -165,45 +165,42 @@ export function getUnsupportedPlatforms(moduleDescription, selectedPlatforms) { return selectedPlatforms.filter((platform) => !moduleDescription.supportedPlatforms.includes(platform)); } -function collectKickModuleEnums(selectedModules) { - const enums = []; +function collectKickModuleMethods(selectedModules) { + const methods = []; selectedModules.forEach((module) => { - if (module.kickModuleEnum) { - enums.push(module.kickModuleEnum); + if (module.kickModuleMethod) { + methods.push(module.kickModuleMethod); } - if (Array.isArray(module.extraKickModuleEnums)) { - module.extraKickModuleEnums.forEach((entry) => { + if (Array.isArray(module.extraKickModuleMethods)) { + module.extraKickModuleMethods.forEach((entry) => { if (entry) { - enums.push(entry); + methods.push(entry); } }); } }); - return unique(enums); + return unique(methods); } function buildGradleSnippet(selectedModules, kickVersion) { - const enums = collectKickModuleEnums(selectedModules); + const methods = collectKickModuleMethods(selectedModules); const lines = []; - lines.push("import ru.bartwell.kick.gradle.KickEnabled"); - lines.push("import ru.bartwell.kick.gradle.KickModule"); - lines.push(""); lines.push("plugins {"); lines.push(` id(\"ru.bartwell.kick\") version \"${kickVersion}\"`); lines.push("}"); lines.push(""); lines.push("kick {"); - lines.push(" enabled = KickEnabled.Auto"); - lines.push(" modules("); - if (enums.length > 0) { - enums.forEach((entry) => { - lines.push(` ${entry},`); + lines.push(" enabledAuto()"); + lines.push(" modules {"); + if (methods.length > 0) { + methods.forEach((method) => { + lines.push(` ${method}();`); }); } else { - lines.push(" // Select at least one module supported by KickModule enum."); + lines.push(" // Select at least one module, e.g. fileExplorer(), ktor3()"); } - lines.push(" )"); + lines.push(" }"); lines.push("}"); lines.push(""); lines.push("// Enable/disable strategy:"); diff --git a/content/wizard/data/modules.json b/content/wizard/data/modules.json index 2a097735..582a3678 100644 --- a/content/wizard/data/modules.json +++ b/content/wizard/data/modules.json @@ -3,8 +3,8 @@ "id": "sqldelight", "titleKey": "module.sqldelight.title", "descriptionKey": "module.sqldelight.description", - "kickModuleEnum": "KickModule.SqliteRuntime", - "extraKickModuleEnums": ["KickModule.SqliteSqlDelightAdapter"], + "kickModuleMethod": "sqldelight", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "medium", @@ -19,8 +19,8 @@ "id": "room", "titleKey": "module.room.title", "descriptionKey": "module.room.description", - "kickModuleEnum": "KickModule.SqliteRuntime", - "extraKickModuleEnums": ["KickModule.SqliteRoomAdapter"], + "kickModuleMethod": "room", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm"], "needsPlatformGlue": false, "configComplexity": "medium", @@ -35,8 +35,8 @@ "id": "logging", "titleKey": "module.logging.title", "descriptionKey": "module.logging.description", - "kickModuleEnum": "KickModule.Logging", - "extraKickModuleEnums": [], + "kickModuleMethod": "logging", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "low", @@ -51,8 +51,8 @@ "id": "ktor3", "titleKey": "module.ktor3.title", "descriptionKey": "module.ktor3.description", - "kickModuleEnum": "KickModule.Ktor3", - "extraKickModuleEnums": [], + "kickModuleMethod": "ktor3", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "low", @@ -67,8 +67,8 @@ "id": "control_panel", "titleKey": "module.control_panel.title", "descriptionKey": "module.control_panel.description", - "kickModuleEnum": null, - "extraKickModuleEnums": [], + "kickModuleMethod": "controlPanel", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "high", @@ -83,8 +83,8 @@ "id": "multiplatform_settings", "titleKey": "module.multiplatform_settings.title", "descriptionKey": "module.multiplatform_settings.description", - "kickModuleEnum": "KickModule.MultiplatformSettings", - "extraKickModuleEnums": [], + "kickModuleMethod": "multiplatformSettings", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "low", @@ -99,8 +99,8 @@ "id": "file_explorer", "titleKey": "module.file_explorer.title", "descriptionKey": "module.file_explorer.description", - "kickModuleEnum": "KickModule.FileExplorer", - "extraKickModuleEnums": [], + "kickModuleMethod": "fileExplorer", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "low", @@ -115,8 +115,8 @@ "id": "layout", "titleKey": "module.layout.title", "descriptionKey": "module.layout.description", - "kickModuleEnum": "KickModule.Layout", - "extraKickModuleEnums": [], + "kickModuleMethod": "layout", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm"], "needsPlatformGlue": false, "configComplexity": "medium", @@ -131,8 +131,8 @@ "id": "overlay", "titleKey": "module.overlay.title", "descriptionKey": "module.overlay.description", - "kickModuleEnum": null, - "extraKickModuleEnums": [], + "kickModuleMethod": "overlay", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "medium", @@ -147,8 +147,8 @@ "id": "firebase_cloud_messaging", "titleKey": "module.firebase_cloud_messaging.title", "descriptionKey": "module.firebase_cloud_messaging.description", - "kickModuleEnum": "KickModule.FirebaseCloudMessaging", - "extraKickModuleEnums": [], + "kickModuleMethod": "firebaseCloudMessaging", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios"], "needsPlatformGlue": true, "configComplexity": "medium", @@ -163,8 +163,8 @@ "id": "firebase_analytics", "titleKey": "module.firebase_analytics.title", "descriptionKey": "module.firebase_analytics.description", - "kickModuleEnum": null, - "extraKickModuleEnums": [], + "kickModuleMethod": "firebaseAnalytics", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios"], "needsPlatformGlue": true, "configComplexity": "medium", @@ -179,8 +179,8 @@ "id": "runner", "titleKey": "module.runner.title", "descriptionKey": "module.runner.description", - "kickModuleEnum": null, - "extraKickModuleEnums": [], + "kickModuleMethod": "runner", + "extraKickModuleMethods": [], "supportedPlatforms": ["android", "ios", "jvm", "wasm"], "needsPlatformGlue": false, "configComplexity": "low", diff --git a/gradle-plugin/src/main/kotlin/org/gradle/kotlin/dsl/Project.kt b/gradle-plugin/src/main/kotlin/org/gradle/kotlin/dsl/Project.kt index 4a53ad45..d4389ce3 100644 --- a/gradle-plugin/src/main/kotlin/org/gradle/kotlin/dsl/Project.kt +++ b/gradle-plugin/src/main/kotlin/org/gradle/kotlin/dsl/Project.kt @@ -5,13 +5,13 @@ import ru.bartwell.kick.gradle.KickExtension /** * Overrides Kick enabled mode from the `kick { }` block. - * - [enabled] = true → use full runtime (equivalent to KickEnabled.Enabled) - * - [enabled] = false → use stub (equivalent to KickEnabled.Disabled) + * - [enabled] = true → use full runtime (equivalent to kick { enabled() }) + * - [enabled] = false → use stub (equivalent to kick { disabled() }) * * Callable in build.gradle.kts without explicit import (package org.gradle.kotlin.dsl is imported by default). * Does not depend on order: safe to call before or after applying the Kick plugin (e.g. in subprojects {}). * - * Priority: CLI -Pkick.enabled > enableKick() > kick { enabled } + * Priority: CLI -Pkick.enabled > enableKick() > kick { enabledAuto() / enabled() / disabled() } */ fun Project.enableKick(enabled: Boolean) { extensions.extraProperties.set(KickExtension.KICK_OVERRIDE_KEY, enabled) diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt index 2643cc27..c3be1875 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt @@ -8,14 +8,14 @@ import org.gradle.api.provider.SetProperty /** * Extension name: `kick` - * DSL: kick { enabled = KickEnabled.Auto; modules(KickModule.FileExplorer) } + * DSL: kick { enabledAuto(); modules { fileExplorer(); ktor3() } } */ abstract class KickExtension( private val project: Project ) { - abstract val enabled: Property + internal abstract val enabled: Property abstract val version: Property - abstract val modules: SetProperty + internal abstract val modules: SetProperty /** * For tests only: if set, used instead of gradleProperty("kick.enabled") so CLI tests can run without a real Gradle property. @@ -34,10 +34,20 @@ abstract class KickExtension( return project.plugins.findPlugin("ru.bartwell.kick")?.javaClass?.`package`?.implementationVersion ?: "1.0.0" } - fun modules(vararg m: KickModule) = modules.addAll(m.asList()) + /** Auto: use task names to decide (release/production/prod → stub). */ + fun enabledAuto() = enabled.set(KickEnabled.Auto) + + /** Always use full runtime (debug). */ + fun enabled() = enabled.set(KickEnabled.Enabled) + + /** Always use stub (release). */ + fun disabled() = enabled.set(KickEnabled.Disabled) + + /** Configure modules via type-safe method calls. */ + fun modules(block: KickModulesScope.() -> Unit) = KickModulesScope(modules).block() /** - * Effective enabled: CLI > enableKick() (extraProperties) > kick { enabled } + * Effective enabled: CLI > enableKick() (extraProperties) > kick { enabledAuto() / enabled() / disabled() } */ fun effectiveEnabled(): KickEnabled { val cli = parseCliOverride() @@ -103,7 +113,7 @@ abstract class KickExtension( val mods = modules.getOrElse(emptySet()) if (mods.isEmpty()) { throw GradleException( - "Kick: modules(...) is required. Example: kick { modules(KickModule.FileExplorer) }" + "Kick: modules { ... } is required. Example: kick { modules { fileExplorer(); ktor3() } }" ) } } diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModule.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModule.kt index c2fa2fa1..90a8a985 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModule.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModule.kt @@ -1,16 +1,20 @@ package ru.bartwell.kick.gradle /** - * Kick feature modules. Add the ones you need via `kick { modules(...) }`. + * Kick feature modules. Add the ones you need via `kick { modules { fileExplorer(); ktor3() } }`. + * Room and Sqldelight pull in sqlite-runtime (and sqlite-core) under the hood. */ enum class KickModule { + ControlPanel, + FileExplorer, + FirebaseAnalytics, + FirebaseCloudMessaging, Ktor3, - SqliteRuntime, - SqliteSqlDelightAdapter, - SqliteRoomAdapter, + Layout, Logging, MultiplatformSettings, - FileExplorer, - Layout, - FirebaseCloudMessaging + Overlay, + Room, + Runner, + Sqldelight, } diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt new file mode 100644 index 00000000..fd5ebbfe --- /dev/null +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt @@ -0,0 +1,25 @@ +package ru.bartwell.kick.gradle + +import org.gradle.api.provider.SetProperty + +/** + * DSL scope for `kick { modules { ... } }`. Use method calls instead of enum references + * so build scripts don't need plugin types on classpath. + * room() and sqldelight() pull in sqlite-runtime (and sqlite-core) under the hood. + */ +class KickModulesScope( + private val modules: SetProperty +) { + fun controlPanel() = modules.add(KickModule.ControlPanel) + fun fileExplorer() = modules.add(KickModule.FileExplorer) + fun firebaseAnalytics() = modules.add(KickModule.FirebaseAnalytics) + fun firebaseCloudMessaging() = modules.add(KickModule.FirebaseCloudMessaging) + fun ktor3() = modules.add(KickModule.Ktor3) + fun layout() = modules.add(KickModule.Layout) + fun logging() = modules.add(KickModule.Logging) + fun multiplatformSettings() = modules.add(KickModule.MultiplatformSettings) + fun overlay() = modules.add(KickModule.Overlay) + fun room() = modules.add(KickModule.Room) + fun runner() = modules.add(KickModule.Runner) + fun sqldelight() = modules.add(KickModule.Sqldelight) +} diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt index 73a3ab6e..1c31fede 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt @@ -2,7 +2,7 @@ package ru.bartwell.kick.gradle /** * Maps [KickModule] to full Maven coordinates (group:artifact:version). - * For SqliteRuntime in runtime mode we also need sqlite-core. + * Room and Sqldelight include sqlite-runtime (and sqlite-core for runtime) under the hood. */ internal object ModuleArtifacts { private const val GROUP = "ru.bartwell.kick" @@ -13,29 +13,35 @@ internal object ModuleArtifacts { fun runtimeArtifacts(module: KickModule, version: String): List { return when (module) { + KickModule.ControlPanel -> listOf("$GROUP:control-panel:$version") + KickModule.FileExplorer -> listOf("$GROUP:file-explorer:$version") + KickModule.FirebaseAnalytics -> listOf("$GROUP:firebase-analytics:$version") + KickModule.FirebaseCloudMessaging -> listOf("$GROUP:firebase-cloud-messaging:$version") KickModule.Ktor3 -> listOf("$GROUP:ktor3:$version") - KickModule.SqliteRuntime -> listOf("$GROUP:sqlite-core:$version", "$GROUP:sqlite-runtime:$version") - KickModule.SqliteSqlDelightAdapter -> listOf("$GROUP:sqlite-sqldelight-adapter:$version") - KickModule.SqliteRoomAdapter -> listOf("$GROUP:sqlite-room-adapter:$version") + KickModule.Layout -> listOf("$GROUP:layout:$version") KickModule.Logging -> listOf("$GROUP:logging:$version") KickModule.MultiplatformSettings -> listOf("$GROUP:multiplatform-settings:$version") - KickModule.FileExplorer -> listOf("$GROUP:file-explorer:$version") - KickModule.Layout -> listOf("$GROUP:layout:$version") - KickModule.FirebaseCloudMessaging -> listOf("$GROUP:firebase-cloud-messaging:$version") + KickModule.Overlay -> listOf("$GROUP:overlay:$version") + KickModule.Room -> listOf("$GROUP:sqlite-core:$version", "$GROUP:sqlite-runtime:$version", "$GROUP:sqlite-room-adapter:$version") + KickModule.Runner -> listOf("$GROUP:runner:$version") + KickModule.Sqldelight -> listOf("$GROUP:sqlite-core:$version", "$GROUP:sqlite-runtime:$version", "$GROUP:sqlite-sqldelight-adapter:$version") } } fun stubArtifacts(module: KickModule, version: String): List { return when (module) { + KickModule.ControlPanel -> listOf("$GROUP:control-panel-stub:$version") + KickModule.FileExplorer -> listOf("$GROUP:file-explorer-stub:$version") + KickModule.FirebaseAnalytics -> listOf("$GROUP:firebase-analytics-stub:$version") + KickModule.FirebaseCloudMessaging -> listOf("$GROUP:firebase-cloud-messaging-stub:$version") KickModule.Ktor3 -> listOf("$GROUP:ktor3-stub:$version") - KickModule.SqliteRuntime -> listOf("$GROUP:sqlite-runtime-stub:$version") - KickModule.SqliteSqlDelightAdapter -> listOf("$GROUP:sqlite-sqldelight-adapter-stub:$version") - KickModule.SqliteRoomAdapter -> listOf("$GROUP:sqlite-room-adapter-stub:$version") + KickModule.Layout -> listOf("$GROUP:layout-stub:$version") KickModule.Logging -> listOf("$GROUP:logging-stub:$version") KickModule.MultiplatformSettings -> listOf("$GROUP:multiplatform-settings-stub:$version") - KickModule.FileExplorer -> listOf("$GROUP:file-explorer-stub:$version") - KickModule.Layout -> listOf("$GROUP:layout-stub:$version") - KickModule.FirebaseCloudMessaging -> listOf("$GROUP:firebase-cloud-messaging-stub:$version") + KickModule.Overlay -> listOf("$GROUP:overlay-stub:$version") + KickModule.Room -> listOf("$GROUP:sqlite-runtime-stub:$version", "$GROUP:sqlite-room-adapter-stub:$version") + KickModule.Runner -> listOf("$GROUP:runner-stub:$version") + KickModule.Sqldelight -> listOf("$GROUP:sqlite-runtime-stub:$version", "$GROUP:sqlite-sqldelight-adapter-stub:$version") } } } diff --git a/gradle-plugin/src/test/kotlin/ru/bartwell/kick/gradle/KickExtensionTest.kt b/gradle-plugin/src/test/kotlin/ru/bartwell/kick/gradle/KickExtensionTest.kt index eaad0a69..47aa08ef 100644 --- a/gradle-plugin/src/test/kotlin/ru/bartwell/kick/gradle/KickExtensionTest.kt +++ b/gradle-plugin/src/test/kotlin/ru/bartwell/kick/gradle/KickExtensionTest.kt @@ -29,8 +29,8 @@ class KickExtensionTest { fun `effectiveEnabled - extension only - Auto`() { val p = createProject() val ext = createExtension(p) - ext.enabled.set(KickEnabled.Auto) - ext.modules(KickModule.FileExplorer) + ext.enabledAuto() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Auto, ext.effectiveEnabled()) } @@ -38,8 +38,8 @@ class KickExtensionTest { fun `effectiveEnabled - extension only - Enabled`() { val p = createProject() val ext = createExtension(p) - ext.enabled.set(KickEnabled.Enabled) - ext.modules(KickModule.FileExplorer) + ext.enabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Enabled, ext.effectiveEnabled()) } @@ -47,8 +47,8 @@ class KickExtensionTest { fun `effectiveEnabled - extension only - Disabled`() { val p = createProject() val ext = createExtension(p) - ext.enabled.set(KickEnabled.Disabled) - ext.modules(KickModule.FileExplorer) + ext.disabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Disabled, ext.effectiveEnabled()) } @@ -57,8 +57,8 @@ class KickExtensionTest { val p = createProject() p.extensions.extraProperties.set(KickExtension.KICK_OVERRIDE_KEY, true) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Disabled) - ext.modules(KickModule.FileExplorer) + ext.disabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Enabled, ext.effectiveEnabled()) } @@ -67,8 +67,8 @@ class KickExtensionTest { val p = createProject() p.extensions.extraProperties.set(KickExtension.KICK_OVERRIDE_KEY, false) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Enabled) - ext.modules(KickModule.FileExplorer) + ext.enabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Disabled, ext.effectiveEnabled()) } @@ -76,8 +76,8 @@ class KickExtensionTest { fun `effectiveEnabled - CLI true wins over enableKick and extension`() { val p = createProject() val ext = createExtensionWithCliOverride(p, "true") - ext.enabled.set(KickEnabled.Disabled) - ext.modules(KickModule.FileExplorer) + ext.disabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Enabled, ext.effectiveEnabled()) } @@ -85,8 +85,8 @@ class KickExtensionTest { fun `effectiveEnabled - CLI false wins over enableKick and extension`() { val p = createProject() val ext = createExtensionWithCliOverride(p, "false") - ext.enabled.set(KickEnabled.Enabled) - ext.modules(KickModule.FileExplorer) + ext.enabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Disabled, ext.effectiveEnabled()) } @@ -95,7 +95,7 @@ class KickExtensionTest { val p = createProject() p.extensions.extraProperties.set(KickExtension.KICK_OVERRIDE_KEY, true) val ext = createExtensionWithCliOverride(p, "false") - ext.modules(KickModule.FileExplorer) + ext.modules { fileExplorer() } assertEquals(KickEnabled.Disabled, ext.effectiveEnabled()) } @@ -103,7 +103,7 @@ class KickExtensionTest { fun `parseCliOverride - invalid value throws`() { val p = createProject() val ext = createExtensionWithCliOverride(p, "yes") - ext.modules(KickModule.FileExplorer) + ext.modules { fileExplorer() } val ex = assertThrows(GradleException::class.java) { ext.effectiveEnabled() } assertEquals("Kick: invalid -Pkick.enabled value. Use true or false.", ex.message) } @@ -112,12 +112,12 @@ class KickExtensionTest { fun `parseCliOverride - true and false case insensitive`() { val pTrue = createProject() val extTrue = createExtensionWithCliOverride(pTrue, "TRUE") - extTrue.modules(KickModule.FileExplorer) + extTrue.modules { fileExplorer() } assertEquals(KickEnabled.Enabled, extTrue.effectiveEnabled()) val pFalse = createProject() val extFalse = createExtensionWithCliOverride(pFalse, "FALSE") - extFalse.modules(KickModule.FileExplorer) + extFalse.modules { fileExplorer() } assertEquals(KickEnabled.Disabled, extFalse.effectiveEnabled()) } @@ -127,7 +127,7 @@ class KickExtensionTest { val ext = createExtension(p) val ex = assertThrows(GradleException::class.java) { ext.validate() } assertEquals( - "Kick: modules(...) is required. Example: kick { modules(KickModule.FileExplorer) }", + "Kick: modules { ... } is required. Example: kick { modules { fileExplorer(); ktor3() } }", ex.message ) } @@ -136,7 +136,7 @@ class KickExtensionTest { fun `validate - at least one module passes`() { val p = createProject() val ext = createExtension(p) - ext.modules(KickModule.FileExplorer) + ext.modules { fileExplorer() } assertDoesNotThrow { ext.validate() } } @@ -145,8 +145,8 @@ class KickExtensionTest { val p = createProject() p.extensions.extraProperties.set(KickExtension.KICK_OVERRIDE_KEY, true) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Disabled) - ext.modules(KickModule.FileExplorer) + ext.disabled() + ext.modules { fileExplorer() } assertEquals(KickEnabled.Enabled, ext.effectiveEnabled()) } @@ -155,8 +155,8 @@ class KickExtensionTest { val p = createProject() p.gradle.startParameter.setTaskNames(listOf("assembleRelease")) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Auto) - ext.modules(KickModule.FileExplorer) + ext.enabledAuto() + ext.modules { fileExplorer() } assertEquals(true, ext.isRelease()) } @@ -165,8 +165,8 @@ class KickExtensionTest { val p = createProject() p.gradle.startParameter.setTaskNames(listOf("linkReleaseFrameworkIosArm64")) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Auto) - ext.modules(KickModule.FileExplorer) + ext.enabledAuto() + ext.modules { fileExplorer() } assertEquals(true, ext.isRelease()) } @@ -175,8 +175,8 @@ class KickExtensionTest { val p = createProject() p.gradle.startParameter.setTaskNames(listOf("bundleProductionFramework")) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Auto) - ext.modules(KickModule.FileExplorer) + ext.enabledAuto() + ext.modules { fileExplorer() } assertEquals(true, ext.isRelease()) } @@ -185,8 +185,8 @@ class KickExtensionTest { val p = createProject() p.gradle.startParameter.setTaskNames(listOf("assembleDebug")) val ext = createExtension(p) - ext.enabled.set(KickEnabled.Auto) - ext.modules(KickModule.FileExplorer) + ext.enabledAuto() + ext.modules { fileExplorer() } assertEquals(false, ext.isRelease()) } } diff --git a/sample/plugin-sample/build.gradle.kts b/sample/plugin-sample/build.gradle.kts index 3f9455dd..334daebc 100644 --- a/sample/plugin-sample/build.gradle.kts +++ b/sample/plugin-sample/build.gradle.kts @@ -12,8 +12,10 @@ plugins { } kick { - enabled = ru.bartwell.kick.gradle.KickEnabled.Auto - modules(ru.bartwell.kick.gradle.KickModule.FileExplorer) + enabledAuto() + modules { + fileExplorer() + } } kotlin { From c91f1c37b95da935b709d216e0c929413e5c254e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sun, 15 Feb 2026 02:57:30 +0300 Subject: [PATCH 04/10] Replace pluginManager.withPlugin() + afterEvaluate() with gradle.projectsEvaluated() to avoid triggering configuration of other projects and ensure proper configuration order in multi-project builds --- .../bartwell/kick/gradle/KickGradlePlugin.kt | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt index 8f0a6267..39a079bc 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt @@ -12,59 +12,59 @@ class KickGradlePlugin : Plugin { override fun apply(project: Project) { val kickExt = project.extensions.create("kick", KickExtension::class.java, project) // version default is set in extension from plugin's Implementation-Version (version.properties at build time) - - project.pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { - configureKick(project, kickExt) - } - - project.afterEvaluate { + // Do not touch other projects or trigger their configuration here (no rootProject/subprojects/project(":…")). + // Defer all logic that might affect configuration order to projectsEvaluated. + project.gradle.projectsEvaluated { if (!project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { throw GradleException( "Kick: Kotlin Multiplatform plugin is required (id(\"org.jetbrains.kotlin.multiplatform\"))." ) } + configureKick(project, kickExt) } } + /** + * Called from projectsEvaluated; all projects are already configured, so we don't use afterEvaluate + * and don't trigger configuration of other projects. + */ private fun configureKick(project: Project, kickExt: KickExtension) { - project.afterEvaluate { - kickExt.validate() - val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java) - val version = kickExt.kickVersion() - val isRelease = kickExt.isRelease() - val mods = kickExt.modules.getOrElse(emptySet()) + kickExt.validate() + val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val version = kickExt.kickVersion() + val isRelease = kickExt.isRelease() + val mods = kickExt.modules.getOrElse(emptySet()) - // Dependencies in commonMain - val commonMain = kotlin.sourceSets.findByName("commonMain") - ?: throw GradleException( - "Kick: commonMain source set not found. " + - "Ensure Kotlin Multiplatform targets are configured." - ) - commonMain.dependencies { - implementation(ModuleArtifacts.mainCore(version)) - if (isRelease) { - implementation(ModuleArtifacts.mainRuntimeStub(version)) - mods.forEach { m -> - ModuleArtifacts.stubArtifacts(m, version).forEach { implementation(it) } - } - } else { - implementation(ModuleArtifacts.mainRuntime(version)) - mods.forEach { m -> - ModuleArtifacts.runtimeArtifacts(m, version).forEach { implementation(it) } - } + // Dependencies in commonMain + val commonMain = kotlin.sourceSets.findByName("commonMain") + ?: throw GradleException( + "Kick: commonMain source set not found. " + + "Ensure Kotlin Multiplatform targets are configured." + ) + commonMain.dependencies { + implementation(ModuleArtifacts.mainCore(version)) + if (isRelease) { + implementation(ModuleArtifacts.mainRuntimeStub(version)) + mods.forEach { m -> + ModuleArtifacts.stubArtifacts(m, version).forEach { implementation(it) } + } + } else { + implementation(ModuleArtifacts.mainRuntime(version)) + mods.forEach { m -> + ModuleArtifacts.runtimeArtifacts(m, version).forEach { implementation(it) } } } + } - // Framework export for all Kotlin/Native framework binaries - val mainCoreDep = project.dependencies.create(ModuleArtifacts.mainCore(version)) - val runtimeDep = project.dependencies.create( - if (isRelease) ModuleArtifacts.mainRuntimeStub(version) else ModuleArtifacts.mainRuntime(version) - ) - kotlin.targets.withType(KotlinNativeTarget::class.java).configureEach { target -> - target.binaries.withType(Framework::class.java).configureEach { framework -> - framework.export(mainCoreDep) - framework.export(runtimeDep) - } + // Framework export for all Kotlin/Native framework binaries + val mainCoreDep = project.dependencies.create(ModuleArtifacts.mainCore(version)) + val runtimeDep = project.dependencies.create( + if (isRelease) ModuleArtifacts.mainRuntimeStub(version) else ModuleArtifacts.mainRuntime(version) + ) + kotlin.targets.withType(KotlinNativeTarget::class.java).configureEach { target -> + target.binaries.withType(Framework::class.java).configureEach { framework -> + framework.export(mainCoreDep) + framework.export(runtimeDep) } } } From 9549c8e9057ee2528a44043cb6ffc40bd8ea51c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sun, 15 Feb 2026 13:12:21 +0300 Subject: [PATCH 05/10] Simplify dependency management in Kick Gradle plugin --- .../bartwell/kick/gradle/KickGradlePlugin.kt | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt index 39a079bc..4f206d4f 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickGradlePlugin.kt @@ -34,37 +34,33 @@ class KickGradlePlugin : Plugin { val version = kickExt.kickVersion() val isRelease = kickExt.isRelease() val mods = kickExt.modules.getOrElse(emptySet()) + val runtimeArtifact = + if (isRelease) ModuleArtifacts.mainRuntimeStub(version) else ModuleArtifacts.mainRuntime(version) + val moduleArtifacts = mods.flatMap { module -> + if (isRelease) { + ModuleArtifacts.stubArtifacts(module, version) + } else { + ModuleArtifacts.runtimeArtifacts(module, version) + } + } - // Dependencies in commonMain val commonMain = kotlin.sourceSets.findByName("commonMain") ?: throw GradleException( "Kick: commonMain source set not found. " + "Ensure Kotlin Multiplatform targets are configured." ) commonMain.dependencies { - implementation(ModuleArtifacts.mainCore(version)) - if (isRelease) { - implementation(ModuleArtifacts.mainRuntimeStub(version)) - mods.forEach { m -> - ModuleArtifacts.stubArtifacts(m, version).forEach { implementation(it) } - } - } else { - implementation(ModuleArtifacts.mainRuntime(version)) - mods.forEach { m -> - ModuleArtifacts.runtimeArtifacts(m, version).forEach { implementation(it) } - } - } + api(ModuleArtifacts.mainCore(version)) + api(runtimeArtifact) + moduleArtifacts.forEach { api(it) } } - // Framework export for all Kotlin/Native framework binaries - val mainCoreDep = project.dependencies.create(ModuleArtifacts.mainCore(version)) - val runtimeDep = project.dependencies.create( - if (isRelease) ModuleArtifacts.mainRuntimeStub(version) else ModuleArtifacts.mainRuntime(version) - ) + val exportDeps = (listOf(ModuleArtifacts.mainCore(version), runtimeArtifact) + moduleArtifacts) + .distinct() + .map(project.dependencies::create) kotlin.targets.withType(KotlinNativeTarget::class.java).configureEach { target -> target.binaries.withType(Framework::class.java).configureEach { framework -> - framework.export(mainCoreDep) - framework.export(runtimeDep) + exportDeps.forEach(framework::export) } } } From fbb4904bf7a935d0542da41bce75d3e8b5e12a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sun, 15 Feb 2026 13:23:50 +0300 Subject: [PATCH 06/10] Replace maven-publish with publish convention plugin --- module/settings/control-panel-stub/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/settings/control-panel-stub/build.gradle.kts b/module/settings/control-panel-stub/build.gradle.kts index 435006ca..11fb0ece 100644 --- a/module/settings/control-panel-stub/build.gradle.kts +++ b/module/settings/control-panel-stub/build.gradle.kts @@ -6,8 +6,8 @@ plugins { alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlinSerialization) - id("maven-publish") - id("signing") + alias(libs.plugins.publish.plugin) + id("publish-convention") } group = "ru.bartwell.kick" From 159688117568d5b46434d0cf74906090ce660326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sun, 15 Feb 2026 13:29:30 +0300 Subject: [PATCH 07/10] Minor note about usage --- README.md | 2 +- content/docs/Advanced.md | 2 +- content/wizard/app/output.js | 2 +- .../main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt | 6 +++++- sample/plugin-sample/build.gradle.kts | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fdc1caed..48196db6 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ plugins { } kick { - enabledAuto() + enabledAuto() // or enabled() / disabled() modules { fileExplorer() } diff --git a/content/docs/Advanced.md b/content/docs/Advanced.md index fded0546..a58f3a24 100644 --- a/content/docs/Advanced.md +++ b/content/docs/Advanced.md @@ -30,7 +30,7 @@ plugins { } kick { - enabledAuto() + enabledAuto() // or enabled() / disabled() modules { controlPanel() fileExplorer() diff --git a/content/wizard/app/output.js b/content/wizard/app/output.js index ec6798b5..90ee0bfb 100644 --- a/content/wizard/app/output.js +++ b/content/wizard/app/output.js @@ -191,7 +191,7 @@ function buildGradleSnippet(selectedModules, kickVersion) { lines.push("}"); lines.push(""); lines.push("kick {"); - lines.push(" enabledAuto()"); + lines.push(" enabledAuto() // or enabled() / disabled()"); lines.push(" modules {"); if (methods.length > 0) { methods.forEach((method) => { diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt index c3be1875..58e61a14 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickExtension.kt @@ -8,7 +8,11 @@ import org.gradle.api.provider.SetProperty /** * Extension name: `kick` - * DSL: kick { enabledAuto(); modules { fileExplorer(); ktor3() } } + * DSL: + * kick { + * enabledAuto() // or enabled() / disabled() + * modules { fileExplorer(); ktor3() } + * } */ abstract class KickExtension( private val project: Project diff --git a/sample/plugin-sample/build.gradle.kts b/sample/plugin-sample/build.gradle.kts index 334daebc..c4c3de37 100644 --- a/sample/plugin-sample/build.gradle.kts +++ b/sample/plugin-sample/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } kick { - enabledAuto() + enabledAuto() // or enabled() / disabled() modules { fileExplorer() } From 07929792046ab7a06d288303ca897f25971fbf94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Sun, 15 Feb 2026 13:40:13 +0300 Subject: [PATCH 08/10] Normalize line endings in output.js --- content/wizard/app/output.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/content/wizard/app/output.js b/content/wizard/app/output.js index 90ee0bfb..d40a613d 100644 --- a/content/wizard/app/output.js +++ b/content/wizard/app/output.js @@ -195,7 +195,7 @@ function buildGradleSnippet(selectedModules, kickVersion) { lines.push(" modules {"); if (methods.length > 0) { methods.forEach((method) => { - lines.push(` ${method}();`); + lines.push(` ${method}()`); }); } else { lines.push(" // Select at least one module, e.g. fileExplorer(), ktor3()"); @@ -274,8 +274,6 @@ function buildCommonSnippet(state, selectedModules, hasPlatformBridge) { if (module.id === "ktor3") { imports.add("ru.bartwell.kick.module.ktor3.Ktor3Module"); moduleLines.push(" module(Ktor3Module(context))"); - moduleLines.push(" // Ktor client integration (outside Kick.init):"); - moduleLines.push(" // install(KickKtor3Plugin)"); return; } From a9ea49bdc60b2ce950d3c391b36ac25c07bbbfdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Mon, 16 Feb 2026 18:07:37 +0300 Subject: [PATCH 09/10] Fix iOS Firebase Cloud Messaging module --- content/docs/Advanced.md | 21 ++++++++++++++ content/wizard/app.js | 11 +++++++ content/wizard/i18n/en.json | 8 ++--- content/wizard/i18n/es.json | 8 ++--- content/wizard/i18n/ja.json | 8 ++--- content/wizard/i18n/pt.json | 8 ++--- content/wizard/i18n/ru.json | 8 ++--- content/wizard/i18n/zh-Hans.json | 8 ++--- .../bartwell/kick/gradle/KickModulesScope.kt | 1 + .../bartwell/kick/gradle/ModuleArtifacts.kt | 17 +++++++++-- .../analytics/core/persist/DatabaseBuilder.kt | 3 -- .../build.gradle.kts | 1 - .../firebase-cloud-messaging/build.gradle.kts | 1 - .../core/persist/DatabaseBuilder.kt | 3 -- .../util/FirebaseExternalUpdates.android.kt | 20 +++++++++++++ .../core/util/FirebaseWrapper.android.kt | 2 ++ .../core/data/FirebaseMessage.kt | 4 +-- .../core/util/FirebaseExternalUpdates.kt | 11 +++++++ .../core/util/FirebaseWrapper.kt | 2 ++ .../DefaultFirebaseCloudMessagingComponent.kt | 29 +++++++++++++++++-- .../FirebaseCloudMessagingAccessor.ios.kt | 9 ++++++ .../core/util/FirebaseExternalUpdates.ios.kt | 19 ++++++++++++ .../core/util/FirebaseWrapper.ios.kt | 21 ++++++++++---- .../logging/core/persist/DatabaseBuilder.kt | 3 -- .../ktor3/core/persist/DatabaseBuilder.kt | 3 -- 25 files changed, 178 insertions(+), 51 deletions(-) create mode 100644 module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.android.kt create mode 100644 module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.kt create mode 100644 module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.ios.kt diff --git a/content/docs/Advanced.md b/content/docs/Advanced.md index a58f3a24..efc488e0 100644 --- a/content/docs/Advanced.md +++ b/content/docs/Advanced.md @@ -288,6 +288,27 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent } ``` +**iOS (Token/FID):** the module does not link Firebase SDK on iOS. +Pass values from your existing Firebase integration (CocoaPods, SPM, manual). + +```swift +import FirebaseMessaging +import FirebaseInstallations +import shared + +// FCM token +Messaging.messaging().token { token, _ in + KickCompanion.shared.firebaseCloudMessaging.setFcmToken(token: token) +} + +// Firebase Installation ID +Installations.installations().installationID { id, _ in + KickCompanion.shared.firebaseCloudMessaging.setFirebaseInstallationId(id: id) +} +``` + +If the token or installation ID changes, call the same setters again — Kick updates the UI automatically. + ### Firebase Analytics Capture analytics calls made by your app and inspect them inside Kick (events, user id, user properties). diff --git a/content/wizard/app.js b/content/wizard/app.js index 76657597..eb8dea50 100644 --- a/content/wizard/app.js +++ b/content/wizard/app.js @@ -542,6 +542,8 @@ function buildFirebaseCloudMessagingExampleSnippet(item) { [ "// iOS (Swift)", "import UIKit", + "import FirebaseMessaging", + "import FirebaseInstallations", "import shared", "", "class AppDelegate: UIResponder, UIApplicationDelegate {", @@ -555,6 +557,15 @@ function buildFirebaseCloudMessagingExampleSnippet(item) { " completionHandler(.noData)", " }", "}", + "", + "// After FirebaseApp.configure() (or when values become available)", + "Messaging.messaging().token { token, _ in", + " KickCompanion.shared.firebaseCloudMessaging.setFcmToken(token: token)", + "}", + "", + "Installations.installations().installationID { id, _ in", + " KickCompanion.shared.firebaseCloudMessaging.setFirebaseInstallationId(id: id)", + "}", ].join("\n") ); } diff --git a/content/wizard/i18n/en.json b/content/wizard/i18n/en.json index f1a3ec03..d323f76e 100644 --- a/content/wizard/i18n/en.json +++ b/content/wizard/i18n/en.json @@ -173,7 +173,7 @@ }, "hook": { "firebaseCloudMessagingAndroid": "In FirebaseMessagingService.onMessageReceived(...) call Kick.firebaseCloudMessaging.handleFcm(message).", - "firebaseCloudMessagingIos": "When APNS notification is received, call one of Kick.firebaseCloudMessaging.handleApns... handlers from shared Kotlin API bridge.", + "firebaseCloudMessagingIos": "When APNS notification is received, call one of Kick.firebaseCloudMessaging.handleApns... handlers from shared Kotlin API bridge. Also pass FCM token and Firebase Installation ID via KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) and setFirebaseInstallationId(...).", "firebaseAnalytics": "Mirror analytics wrapper calls to Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." }, "unsupportedModule": "{module}: unsupported on this platform.", @@ -192,10 +192,10 @@ "logging": "To send logs to Logging module, call Kick.log(level, message) in the places you want to log.", "loggingWithNapier": "To send logs to Logging module, call Kick.log(level, message) where needed, or route all Napier logs with installNapierBridge() from KickBootstrap.", "ktor3": "To log Ktor3 network activity, add install(KickKtor3Plugin) when you create HttpClient.", - "firebaseCloudMessagingBoth": "For Firebase Cloud Messaging: on Android call Kick.firebaseCloudMessaging.handleFcm(message) inside FirebaseMessagingService.onMessageReceived(...). On iOS call KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) in AppDelegate when APNS payload is received.", + "firebaseCloudMessagingBoth": "For Firebase Cloud Messaging: on Android call Kick.firebaseCloudMessaging.handleFcm(message) inside FirebaseMessagingService.onMessageReceived(...). On iOS call KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) in AppDelegate when APNS payload is received. Also on iOS pass FCM token and Firebase Installation ID via KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) and setFirebaseInstallationId(...).", "firebaseCloudMessagingAndroid": "For Firebase Cloud Messaging on Android: call Kick.firebaseCloudMessaging.handleFcm(message) inside FirebaseMessagingService.onMessageReceived(...).", - "firebaseCloudMessagingIos": "For Firebase Cloud Messaging on iOS: call KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) in AppDelegate when APNS payload is received.", - "firebaseCloudMessagingGeneric": "For Firebase Cloud Messaging, forward push payloads to Kick handlers in your platform lifecycle code.", + "firebaseCloudMessagingIos": "For Firebase Cloud Messaging on iOS: call KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) in AppDelegate when APNS payload is received. Also pass FCM token and Firebase Installation ID via KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) and setFirebaseInstallationId(...).", + "firebaseCloudMessagingGeneric": "For Firebase Cloud Messaging, forward push payloads to Kick handlers in your platform lifecycle code. On iOS also pass FCM token and Firebase Installation ID via KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) and setFirebaseInstallationId(...).", "firebaseAnalytics": "For Firebase Analytics, add Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), and Kick.firebaseAnalytics.setUserProperty(...) in matching places in your analytics wrapper.", "controlPanel": "Read ControlPanel values in your code with Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...), etc. If you added buttons, listen for clicks with Kick.controlPanel.events.collect { event -> ... }.", "overlay": "Show required runtime values in Overlay floating window with Kick.overlay.set(\"key\", value).", diff --git a/content/wizard/i18n/es.json b/content/wizard/i18n/es.json index 20fcbcf6..9b5bb939 100644 --- a/content/wizard/i18n/es.json +++ b/content/wizard/i18n/es.json @@ -173,7 +173,7 @@ }, "hook": { "firebaseCloudMessagingAndroid": "En FirebaseMessagingService.onMessageReceived(...) llama a Kick.firebaseCloudMessaging.handleFcm(message).", - "firebaseCloudMessagingIos": "Cuando se reciba una notificación APNS, llama a uno de los handlers Kick.firebaseCloudMessaging.handleApns... desde el bridge de Kotlin compartido.", + "firebaseCloudMessagingIos": "Cuando se reciba una notificación APNS, llama a uno de los handlers Kick.firebaseCloudMessaging.handleApns... desde el bridge de Kotlin compartido. Además, pasa el token FCM y el Firebase Installation ID mediante KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) y setFirebaseInstallationId(...).", "firebaseAnalytics": "Duplica las llamadas del wrapper de analytics en Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." }, "unsupportedModule": "{module}: no compatible en esta plataforma.", @@ -192,10 +192,10 @@ "logging": "Para enviar logs al módulo Logging, llama a Kick.log(level, message) en los lugares que quieras registrar.", "loggingWithNapier": "Para enviar logs al módulo Logging, llama a Kick.log(level, message) donde haga falta, o redirige todos los logs de Napier con installNapierBridge() desde KickBootstrap.", "ktor3": "Para registrar actividad de red de Ktor3, agrega install(KickKtor3Plugin) al crear HttpClient.", - "firebaseCloudMessagingBoth": "Para Firebase Cloud Messaging: en Android llama a Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...). En iOS llama a KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) en AppDelegate cuando recibas payload APNS.", + "firebaseCloudMessagingBoth": "Para Firebase Cloud Messaging: en Android llama a Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...). En iOS llama a KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) en AppDelegate cuando recibas payload APNS. Además en iOS pasa el token FCM y el Firebase Installation ID mediante KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) y setFirebaseInstallationId(...).", "firebaseCloudMessagingAndroid": "Para Firebase Cloud Messaging en Android: llama a Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...).", - "firebaseCloudMessagingIos": "Para Firebase Cloud Messaging en iOS: llama a KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) en AppDelegate cuando recibas payload APNS.", - "firebaseCloudMessagingGeneric": "Para Firebase Cloud Messaging, reenvía payloads push a los handlers de Kick en el ciclo de vida de la plataforma.", + "firebaseCloudMessagingIos": "Para Firebase Cloud Messaging en iOS: llama a KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) en AppDelegate cuando recibas payload APNS. Además pasa el token FCM y el Firebase Installation ID mediante KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) y setFirebaseInstallationId(...).", + "firebaseCloudMessagingGeneric": "Para Firebase Cloud Messaging, reenvía payloads push a los handlers de Kick en el ciclo de vida de la plataforma. En iOS también pasa el token FCM y el Firebase Installation ID mediante KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) y setFirebaseInstallationId(...).", "firebaseAnalytics": "Para Firebase Analytics, agrega Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), y Kick.firebaseAnalytics.setUserProperty(...) en los mismos puntos de tu wrapper de analytics.", "controlPanel": "Lee valores de ControlPanel en tu código con Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...), etc. Si agregaste botones, escucha clics con Kick.controlPanel.events.collect { event -> ... }.", "overlay": "Muestra valores runtime en la ventana flotante Overlay con Kick.overlay.set(\"key\", value).", diff --git a/content/wizard/i18n/ja.json b/content/wizard/i18n/ja.json index e58f75b9..04d5b375 100644 --- a/content/wizard/i18n/ja.json +++ b/content/wizard/i18n/ja.json @@ -173,7 +173,7 @@ }, "hook": { "firebaseCloudMessagingAndroid": "FirebaseMessagingService.onMessageReceived(...) で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。", - "firebaseCloudMessagingIos": "APNS 通知受信時に、共有 Kotlin API bridge から Kick.firebaseCloudMessaging.handleApns... を呼びます。", + "firebaseCloudMessagingIos": "APNS 通知受信時に、共有 Kotlin API bridge から Kick.firebaseCloudMessaging.handleApns... を呼びます。さらに、FCM トークンと Firebase Installation ID を KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) と setFirebaseInstallationId(...) で渡してください。", "firebaseAnalytics": "analytics wrapper の呼び出しを Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty にもミラーします。" }, "unsupportedModule": "{module}: このプラットフォームでは未対応です。", @@ -192,10 +192,10 @@ "logging": "Logging モジュールにログを送るには、必要な箇所で Kick.log(level, message) を呼び出します。", "loggingWithNapier": "Logging モジュールにログを送るには、必要箇所で Kick.log(level, message) を呼ぶか、KickBootstrap の installNapierBridge() で Napier ログをすべて転送します。", "ktor3": "Ktor3 のネットワーク活動を記録するには、HttpClient 作成時に install(KickKtor3Plugin) を追加します。", - "firebaseCloudMessagingBoth": "Firebase Cloud Messaging: Android では FirebaseMessagingService.onMessageReceived(...) 内で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。iOS では APNS payload 受信時に AppDelegate で KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) を呼びます。", + "firebaseCloudMessagingBoth": "Firebase Cloud Messaging: Android では FirebaseMessagingService.onMessageReceived(...) 内で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。iOS では APNS payload 受信時に AppDelegate で KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) を呼びます。さらに iOS では FCM トークンと Firebase Installation ID を KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) と setFirebaseInstallationId(...) で渡します。", "firebaseCloudMessagingAndroid": "Android の Firebase Cloud Messaging: FirebaseMessagingService.onMessageReceived(...) 内で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。", - "firebaseCloudMessagingIos": "iOS の Firebase Cloud Messaging: APNS payload 受信時に AppDelegate で KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) を呼びます。", - "firebaseCloudMessagingGeneric": "Firebase Cloud Messaging では、プッシュ payload をプラットフォームのライフサイクルコードから Kick ハンドラに転送してください。", + "firebaseCloudMessagingIos": "iOS の Firebase Cloud Messaging: APNS payload 受信時に AppDelegate で KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) を呼びます。さらに、FCM トークンと Firebase Installation ID を KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) と setFirebaseInstallationId(...) で渡します。", + "firebaseCloudMessagingGeneric": "Firebase Cloud Messaging では、プッシュ payload をプラットフォームのライフサイクルコードから Kick ハンドラに転送してください。iOS では FCM トークンと Firebase Installation ID を KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) と setFirebaseInstallationId(...) で渡してください。", "firebaseAnalytics": "Firebase Analytics では、analytics wrapper の対応箇所で Kick.firebaseAnalytics.logEvent(...)、Kick.firebaseAnalytics.setUserId(...)、Kick.firebaseAnalytics.setUserProperty(...) を呼び出します。", "controlPanel": "コード中で Kick.controlPanel.getBoolean(...)、Kick.controlPanel.getString(...) などを使って ControlPanel の値を取得します。ボタンを追加した場合は Kick.controlPanel.events.collect { event -> ... } でクリックを監視します。", "overlay": "Kick.overlay.set(\"key\", value) を使って Overlay のフローティングウィンドウに必要な値を表示します。", diff --git a/content/wizard/i18n/pt.json b/content/wizard/i18n/pt.json index 4922f031..b47ad2a2 100644 --- a/content/wizard/i18n/pt.json +++ b/content/wizard/i18n/pt.json @@ -173,7 +173,7 @@ }, "hook": { "firebaseCloudMessagingAndroid": "Em FirebaseMessagingService.onMessageReceived(...) chame Kick.firebaseCloudMessaging.handleFcm(message).", - "firebaseCloudMessagingIos": "Quando uma notificação APNS for recebida, chame um dos handlers Kick.firebaseCloudMessaging.handleApns... a partir da bridge Kotlin compartilhada.", + "firebaseCloudMessagingIos": "Quando uma notificação APNS for recebida, chame um dos handlers Kick.firebaseCloudMessaging.handleApns... a partir da bridge Kotlin compartilhada. Além disso, passe o token FCM e o Firebase Installation ID por meio de KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) e setFirebaseInstallationId(...).", "firebaseAnalytics": "Espelhe as chamadas do wrapper de analytics em Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." }, "unsupportedModule": "{module}: não suportado nesta plataforma.", @@ -192,10 +192,10 @@ "logging": "Para enviar logs ao módulo Logging, chame Kick.log(level, message) nos pontos que você deseja logar.", "loggingWithNapier": "Para enviar logs ao módulo Logging, chame Kick.log(level, message) onde necessário ou redirecione todos os logs do Napier com installNapierBridge() a partir do KickBootstrap.", "ktor3": "Para logar atividade de rede do Ktor3, adicione install(KickKtor3Plugin) ao criar o HttpClient.", - "firebaseCloudMessagingBoth": "Para Firebase Cloud Messaging: no Android, chame Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...). No iOS, chame KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) no AppDelegate quando um payload APNS for recebido.", + "firebaseCloudMessagingBoth": "Para Firebase Cloud Messaging: no Android, chame Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...). No iOS, chame KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) no AppDelegate quando um payload APNS for recebido. Além disso, no iOS passe o token FCM e o Firebase Installation ID por meio de KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) e setFirebaseInstallationId(...).", "firebaseCloudMessagingAndroid": "Para Firebase Cloud Messaging no Android: chame Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...).", - "firebaseCloudMessagingIos": "Para Firebase Cloud Messaging no iOS: chame KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) no AppDelegate quando um payload APNS for recebido.", - "firebaseCloudMessagingGeneric": "Para Firebase Cloud Messaging, encaminhe payloads push para os handlers do Kick no código de ciclo de vida da plataforma.", + "firebaseCloudMessagingIos": "Para Firebase Cloud Messaging no iOS: chame KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) no AppDelegate quando um payload APNS for recebido. Além disso, passe o token FCM e o Firebase Installation ID por meio de KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) e setFirebaseInstallationId(...).", + "firebaseCloudMessagingGeneric": "Para Firebase Cloud Messaging, encaminhe payloads push para os handlers do Kick no código de ciclo de vida da plataforma. No iOS também passe o token FCM e o Firebase Installation ID por meio de KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) e setFirebaseInstallationId(...).", "firebaseAnalytics": "Para Firebase Analytics, adicione Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), e Kick.firebaseAnalytics.setUserProperty(...) nos mesmos pontos do seu wrapper de analytics.", "controlPanel": "Leia valores do ControlPanel no seu código com Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...), etc. Se você adicionou botões, escute cliques com Kick.controlPanel.events.collect { event -> ... }.", "overlay": "Mostre valores runtime na janela flutuante do Overlay com Kick.overlay.set(\"key\", value).", diff --git a/content/wizard/i18n/ru.json b/content/wizard/i18n/ru.json index 9c420487..878a46f6 100644 --- a/content/wizard/i18n/ru.json +++ b/content/wizard/i18n/ru.json @@ -173,7 +173,7 @@ }, "hook": { "firebaseCloudMessagingAndroid": "В FirebaseMessagingService.onMessageReceived(...) вызывайте Kick.firebaseCloudMessaging.handleFcm(message).", - "firebaseCloudMessagingIos": "При получении APNS вызывать один из обработчиков Kick.firebaseCloudMessaging.handleApns... через bridge к Kotlin API.", + "firebaseCloudMessagingIos": "При получении APNS вызывать один из обработчиков Kick.firebaseCloudMessaging.handleApns... через bridge к Kotlin API. Также передайте FCM-токен и Firebase Installation ID через KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) и setFirebaseInstallationId(...).", "firebaseAnalytics": "Дублируйте вызовы analytics-wrapper в Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." }, "unsupportedModule": "{module}: модуль не поддерживается на этой платформе.", @@ -192,10 +192,10 @@ "logging": "Чтобы отправлять логи в модуль Logging, вызывайте Kick.log(level, message) в нужных местах.", "loggingWithNapier": "Чтобы отправлять логи в модуль Logging, вызывайте Kick.log(level, message) в нужных местах или перенаправьте все логи Napier через installNapierBridge() из KickBootstrap.", "ktor3": "Чтобы логировать сетевую активность Ktor3, добавьте install(KickKtor3Plugin) при создании HttpClient.", - "firebaseCloudMessagingBoth": "Для Firebase Cloud Messaging: на Android вызовите Kick.firebaseCloudMessaging.handleFcm(message) внутри FirebaseMessagingService.onMessageReceived(...). На iOS вызовите KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) в AppDelegate при получении APNS payload.", + "firebaseCloudMessagingBoth": "Для Firebase Cloud Messaging: на Android вызовите Kick.firebaseCloudMessaging.handleFcm(message) внутри FirebaseMessagingService.onMessageReceived(...). На iOS вызовите KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) в AppDelegate при получении APNS payload. Также на iOS передайте FCM-токен и Firebase Installation ID через KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) и setFirebaseInstallationId(...).", "firebaseCloudMessagingAndroid": "Для Firebase Cloud Messaging на Android: вызовите Kick.firebaseCloudMessaging.handleFcm(message) внутри FirebaseMessagingService.onMessageReceived(...).", - "firebaseCloudMessagingIos": "Для Firebase Cloud Messaging на iOS: вызовите KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) в AppDelegate при получении APNS payload.", - "firebaseCloudMessagingGeneric": "Для Firebase Cloud Messaging пробрасывайте push payload в Kick-обработчики в platform lifecycle коде.", + "firebaseCloudMessagingIos": "Для Firebase Cloud Messaging на iOS: вызовите KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) в AppDelegate при получении APNS payload. Также передайте FCM-токен и Firebase Installation ID через KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) и setFirebaseInstallationId(...).", + "firebaseCloudMessagingGeneric": "Для Firebase Cloud Messaging пробрасывайте push payload в Kick-обработчики в platform lifecycle коде. На iOS также передайте FCM-токен и Firebase Installation ID через KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) и setFirebaseInstallationId(...).", "firebaseAnalytics": "Для Firebase Analytics добавьте вызовы Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), Kick.firebaseAnalytics.setUserProperty(...) в соответствующие места вашего analytics-wrapper.", "controlPanel": "Получайте значения ControlPanel в коде через Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...) и т.д. Если добавили кнопки, отслеживайте клики через Kick.controlPanel.events.collect { event -> ... }.", "overlay": "Показывайте нужные runtime-значения в плавающем окне Overlay через Kick.overlay.set(\"key\", value).", diff --git a/content/wizard/i18n/zh-Hans.json b/content/wizard/i18n/zh-Hans.json index cee72ecb..4d514e5e 100644 --- a/content/wizard/i18n/zh-Hans.json +++ b/content/wizard/i18n/zh-Hans.json @@ -173,7 +173,7 @@ }, "hook": { "firebaseCloudMessagingAndroid": "在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。", - "firebaseCloudMessagingIos": "收到 APNS 通知时,通过共享 Kotlin bridge 调用 Kick.firebaseCloudMessaging.handleApns... 处理方法。", + "firebaseCloudMessagingIos": "收到 APNS 通知时,通过共享 Kotlin bridge 调用 Kick.firebaseCloudMessaging.handleApns... 处理方法。同时将 FCM token 和 Firebase Installation ID 通过 KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) 和 setFirebaseInstallationId(...) 传入。", "firebaseAnalytics": "在 analytics wrapper 中同步调用 Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty。" }, "unsupportedModule": "{module}:该平台不支持。", @@ -192,10 +192,10 @@ "logging": "如需将日志发送到 Logging 模块,请在需要的位置调用 Kick.log(level, message)。", "loggingWithNapier": "如需将日志发送到 Logging 模块,请在需要位置调用 Kick.log(level, message),或在 KickBootstrap 中通过 installNapierBridge() 转发全部 Napier 日志。", "ktor3": "要记录 Ktor3 网络活动,请在创建 HttpClient 时添加 install(KickKtor3Plugin)。", - "firebaseCloudMessagingBoth": "Firebase Cloud Messaging:Android 在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。iOS 在收到 APNS payload 时于 AppDelegate 调用 KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)。", + "firebaseCloudMessagingBoth": "Firebase Cloud Messaging:Android 在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。iOS 在收到 APNS payload 时于 AppDelegate 调用 KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)。同时在 iOS 通过 KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) 和 setFirebaseInstallationId(...) 传入 FCM token 和 Firebase Installation ID。", "firebaseCloudMessagingAndroid": "Android 上的 Firebase Cloud Messaging:在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。", - "firebaseCloudMessagingIos": "iOS 上的 Firebase Cloud Messaging:在 AppDelegate 收到 APNS payload 时调用 KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)。", - "firebaseCloudMessagingGeneric": "对于 Firebase Cloud Messaging,请在平台生命周期代码中把 push payload 转发给 Kick 处理器。", + "firebaseCloudMessagingIos": "iOS 上的 Firebase Cloud Messaging:在 AppDelegate 收到 APNS payload 时调用 KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)。同时通过 KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) 和 setFirebaseInstallationId(...) 传入 FCM token 和 Firebase Installation ID。", + "firebaseCloudMessagingGeneric": "对于 Firebase Cloud Messaging,请在平台生命周期代码中把 push payload 转发给 Kick 处理器。在 iOS 还需要通过 KickCompanion.shared.firebaseCloudMessaging.setFcmToken(...) 和 setFirebaseInstallationId(...) 传入 FCM token 和 Firebase Installation ID。", "firebaseAnalytics": "对于 Firebase Analytics,请在你的 analytics wrapper 对应位置添加 Kick.firebaseAnalytics.logEvent(...)、Kick.firebaseAnalytics.setUserId(...) 和 Kick.firebaseAnalytics.setUserProperty(...)。", "controlPanel": "在代码中通过 Kick.controlPanel.getBoolean(...)、Kick.controlPanel.getString(...) 等读取 ControlPanel 值。如添加了按钮,请通过 Kick.controlPanel.events.collect { event -> ... } 监听点击。", "overlay": "通过 Kick.overlay.set(\"key\", value) 在 Overlay 浮窗显示所需运行时数据。", diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt index fd5ebbfe..4bebc07b 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/KickModulesScope.kt @@ -7,6 +7,7 @@ import org.gradle.api.provider.SetProperty * so build scripts don't need plugin types on classpath. * room() and sqldelight() pull in sqlite-runtime (and sqlite-core) under the hood. */ +@Suppress("TooManyFunctions") class KickModulesScope( private val modules: SetProperty ) { diff --git a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt index 1c31fede..d04115ec 100644 --- a/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt +++ b/gradle-plugin/src/main/kotlin/ru/bartwell/kick/gradle/ModuleArtifacts.kt @@ -22,9 +22,17 @@ internal object ModuleArtifacts { KickModule.Logging -> listOf("$GROUP:logging:$version") KickModule.MultiplatformSettings -> listOf("$GROUP:multiplatform-settings:$version") KickModule.Overlay -> listOf("$GROUP:overlay:$version") - KickModule.Room -> listOf("$GROUP:sqlite-core:$version", "$GROUP:sqlite-runtime:$version", "$GROUP:sqlite-room-adapter:$version") + KickModule.Room -> listOf( + "$GROUP:sqlite-core:$version", + "$GROUP:sqlite-runtime:$version", + "$GROUP:sqlite-room-adapter:$version" + ) KickModule.Runner -> listOf("$GROUP:runner:$version") - KickModule.Sqldelight -> listOf("$GROUP:sqlite-core:$version", "$GROUP:sqlite-runtime:$version", "$GROUP:sqlite-sqldelight-adapter:$version") + KickModule.Sqldelight -> listOf( + "$GROUP:sqlite-core:$version", + "$GROUP:sqlite-runtime:$version", + "$GROUP:sqlite-sqldelight-adapter:$version" + ) } } @@ -41,7 +49,10 @@ internal object ModuleArtifacts { KickModule.Overlay -> listOf("$GROUP:overlay-stub:$version") KickModule.Room -> listOf("$GROUP:sqlite-runtime-stub:$version", "$GROUP:sqlite-room-adapter-stub:$version") KickModule.Runner -> listOf("$GROUP:runner-stub:$version") - KickModule.Sqldelight -> listOf("$GROUP:sqlite-runtime-stub:$version", "$GROUP:sqlite-sqldelight-adapter-stub:$version") + KickModule.Sqldelight -> listOf( + "$GROUP:sqlite-runtime-stub:$version", + "$GROUP:sqlite-sqldelight-adapter-stub:$version" + ) } } } diff --git a/module/firebase/firebase-analytics/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/analytics/core/persist/DatabaseBuilder.kt b/module/firebase/firebase-analytics/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/analytics/core/persist/DatabaseBuilder.kt index 45b2f50f..904593b9 100644 --- a/module/firebase/firebase-analytics/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/analytics/core/persist/DatabaseBuilder.kt +++ b/module/firebase/firebase-analytics/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/analytics/core/persist/DatabaseBuilder.kt @@ -17,9 +17,6 @@ internal actual class DatabaseBuilder { context = appContext, name = "kick_firebase_analytics.db" ) - try { - FirebaseAnalyticsDb.Schema.synchronous().create(driver) - } catch (_: RuntimeException) {} val db = FirebaseAnalyticsDb( driver = driver, analyticsEventAdapter = AnalyticsEvent.Adapter( diff --git a/module/firebase/firebase-cloud-messaging-stub/build.gradle.kts b/module/firebase/firebase-cloud-messaging-stub/build.gradle.kts index e825dfc1..798c2574 100644 --- a/module/firebase/firebase-cloud-messaging-stub/build.gradle.kts +++ b/module/firebase/firebase-cloud-messaging-stub/build.gradle.kts @@ -46,7 +46,6 @@ kotlin { implementation(libs.decompose.extensions.compose) implementation(libs.decompose.essenty.lifecycle.coroutines) implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.datetime) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/module/firebase/firebase-cloud-messaging/build.gradle.kts b/module/firebase/firebase-cloud-messaging/build.gradle.kts index 7429e288..7e372a6d 100644 --- a/module/firebase/firebase-cloud-messaging/build.gradle.kts +++ b/module/firebase/firebase-cloud-messaging/build.gradle.kts @@ -47,7 +47,6 @@ kotlin { implementation(libs.decompose.extensions.compose) implementation(libs.decompose.essenty.lifecycle.coroutines) implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.datetime) implementation(libs.sqldelight.coroutines.extensions) implementation(libs.sqldelight.async.extensions) } diff --git a/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/persist/DatabaseBuilder.kt b/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/persist/DatabaseBuilder.kt index b7b01c18..186490ab 100644 --- a/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/persist/DatabaseBuilder.kt +++ b/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/persist/DatabaseBuilder.kt @@ -17,9 +17,6 @@ internal actual class DatabaseBuilder { context = appContext, name = "kick_firebase_cloud_messaging.db", ) - try { - FirebaseCloudMessagingDb.Schema.synchronous().create(driver) - } catch (_: RuntimeException) {} val db = FirebaseCloudMessagingDb( driver = driver, fcmMessageAdapter = FcmMessage.Adapter( diff --git a/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.android.kt b/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.android.kt new file mode 100644 index 00000000..ed8ccc58 --- /dev/null +++ b/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.android.kt @@ -0,0 +1,20 @@ +package ru.bartwell.kick.module.firebase.cloudmessaging.core.util + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal actual object FirebaseExternalUpdates { + private val fcmTokenState = MutableStateFlow(null) + private val installationIdState = MutableStateFlow(null) + + actual val fcmToken: StateFlow = fcmTokenState + actual val installationId: StateFlow = installationIdState + + actual fun updateFcmToken(token: String?) { + fcmTokenState.value = token + } + + actual fun updateInstallationId(id: String?) { + installationIdState.value = id + } +} diff --git a/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.android.kt b/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.android.kt index 0ed410ef..aabbc5c5 100644 --- a/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.android.kt +++ b/module/firebase/firebase-cloud-messaging/src/androidMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.android.kt @@ -12,6 +12,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException internal actual object FirebaseWrapper { + actual val supportsAutoFetch: Boolean = true + actual fun isFirebaseInitialized( context: PlatformContext, ): Boolean { diff --git a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/data/FirebaseMessage.kt b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/data/FirebaseMessage.kt index f2059e42..57ba81e1 100644 --- a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/data/FirebaseMessage.kt +++ b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/data/FirebaseMessage.kt @@ -1,7 +1,7 @@ package ru.bartwell.kick.module.firebase.cloudmessaging.core.data -import kotlinx.datetime.Clock import kotlinx.serialization.Serializable +import ru.bartwell.kick.core.util.DateUtils @Serializable internal data class FirebaseMessage( @@ -23,5 +23,5 @@ internal data class FirebaseMessage( val priority: String? = null, val ttlSeconds: Long? = null, val raw: Map = emptyMap(), - val receivedAtMillis: Long = Clock.System.now().toEpochMilliseconds(), + val receivedAtMillis: Long = DateUtils.currentTimeMillis(), ) diff --git a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.kt b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.kt new file mode 100644 index 00000000..22bbcca6 --- /dev/null +++ b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.kt @@ -0,0 +1,11 @@ +package ru.bartwell.kick.module.firebase.cloudmessaging.core.util + +import kotlinx.coroutines.flow.StateFlow + +internal expect object FirebaseExternalUpdates { + val fcmToken: StateFlow + val installationId: StateFlow + + fun updateFcmToken(token: String?) + fun updateInstallationId(id: String?) +} diff --git a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.kt b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.kt index f2aa3c0f..26658d3e 100644 --- a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.kt +++ b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.kt @@ -3,6 +3,8 @@ package ru.bartwell.kick.module.firebase.cloudmessaging.core.util import ru.bartwell.kick.core.data.PlatformContext internal expect object FirebaseWrapper { + val supportsAutoFetch: Boolean + fun isFirebaseInitialized(context: PlatformContext): Boolean suspend fun getRegistrationToken( diff --git a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/feature/main/presentation/DefaultFirebaseCloudMessagingComponent.kt b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/feature/main/presentation/DefaultFirebaseCloudMessagingComponent.kt index cab8c80f..3a6ff065 100644 --- a/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/feature/main/presentation/DefaultFirebaseCloudMessagingComponent.kt +++ b/module/firebase/firebase-cloud-messaging/src/commonMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/feature/main/presentation/DefaultFirebaseCloudMessagingComponent.kt @@ -4,8 +4,11 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.module.firebase.cloudmessaging.core.util.FirebaseExternalUpdates import ru.bartwell.kick.module.firebase.cloudmessaging.core.util.FirebaseWrapper import ru.bartwell.kick.module.firebase.cloudmessaging.feature.main.extension.copyToClipboard @@ -25,8 +28,11 @@ internal class DefaultFirebaseCloudMessagingComponent( override fun init(context: PlatformContext) { if (ensureFirebaseAvailability(context = context, requireFirebase = false)) { - refreshToken(context = context, forceRefresh = false) - refreshFirebaseId(context = context) + observeExternalValues() + if (FirebaseWrapper.supportsAutoFetch) { + refreshToken(context = context, forceRefresh = false) + refreshFirebaseId(context = context) + } } else { clearRemoteState() } @@ -141,6 +147,25 @@ internal class DefaultFirebaseCloudMessagingComponent( } } + private fun observeExternalValues() { + uiScope.launch { + FirebaseExternalUpdates.fcmToken + .filterNotNull() + .collect { token -> + updateState { copy(token = token, tokenError = null, isTokenLoading = false) } + } + } + uiScope.launch { + FirebaseExternalUpdates.installationId + .filterNotNull() + .collect { id -> + updateState { + copy(firebaseId = id, firebaseIdError = null, isFirebaseIdLoading = false) + } + } + } + } + companion object { private const val NOT_INITIALISED_MESSAGE: String = "Firebase is not initialised in the host application" } diff --git a/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/FirebaseCloudMessagingAccessor.ios.kt b/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/FirebaseCloudMessagingAccessor.ios.kt index 9d4213ea..cd5ba7bf 100644 --- a/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/FirebaseCloudMessagingAccessor.ios.kt +++ b/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/FirebaseCloudMessagingAccessor.ios.kt @@ -3,6 +3,7 @@ package ru.bartwell.kick.module.firebase.cloudmessaging import platform.Foundation.NSDictionary import platform.UserNotifications.UNNotification import ru.bartwell.kick.module.firebase.cloudmessaging.core.data.toFirebaseMessage +import ru.bartwell.kick.module.firebase.cloudmessaging.core.util.FirebaseExternalUpdates import ru.bartwell.kick.module.firebase.cloudmessaging.core.util.FirebaseMessageLogger public fun FirebaseCloudMessagingAccessor.handleApnsPayload(userInfo: NSDictionary) { @@ -17,3 +18,11 @@ public fun FirebaseCloudMessagingAccessor.handleApnsNotification(notification: U val payload = notification.request.content.userInfo FirebaseMessageLogger.log(payload.toFirebaseMessage()) } + +public fun FirebaseCloudMessagingAccessor.setFcmToken(token: String?) { + FirebaseExternalUpdates.updateFcmToken(token) +} + +public fun FirebaseCloudMessagingAccessor.setFirebaseInstallationId(id: String?) { + FirebaseExternalUpdates.updateInstallationId(id) +} diff --git a/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.ios.kt b/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.ios.kt new file mode 100644 index 00000000..dec70b39 --- /dev/null +++ b/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseExternalUpdates.ios.kt @@ -0,0 +1,19 @@ +package ru.bartwell.kick.module.firebase.cloudmessaging.core.util + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +internal actual object FirebaseExternalUpdates { + private val fcmTokenState = MutableStateFlow(null) + private val installationIdState = MutableStateFlow(null) + + actual val fcmToken: StateFlow = fcmTokenState + actual val installationId: StateFlow = installationIdState + + actual fun updateFcmToken(token: String?) { + fcmTokenState.value = token + } + + actual fun updateInstallationId(id: String?) { + installationIdState.value = id + } +} diff --git a/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.ios.kt b/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.ios.kt index dc4d61fc..2a8cd42e 100644 --- a/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.ios.kt +++ b/module/firebase/firebase-cloud-messaging/src/iosMain/kotlin/ru/bartwell/kick/module/firebase/cloudmessaging/core/util/FirebaseWrapper.ios.kt @@ -2,21 +2,30 @@ package ru.bartwell.kick.module.firebase.cloudmessaging.core.util import ru.bartwell.kick.core.data.PlatformContext -private const val IOS_UNAVAILABLE_MESSAGE: String = - "Firebase Cloud Messaging runtime is not included in this build" +private const val TOKEN_UNAVAILABLE_MESSAGE: String = + "FCM token is not available yet. Provide it from the host app." +private const val INSTALLATION_ID_UNAVAILABLE_MESSAGE: String = + "Firebase installation id is not available yet. Provide it from the host app." internal actual object FirebaseWrapper { + actual val supportsAutoFetch: Boolean = false + actual fun isFirebaseInitialized( context: PlatformContext, - ): Boolean = false + ): Boolean = true actual suspend fun getRegistrationToken( context: PlatformContext, forceRefresh: Boolean, - ): Result = Result.failure(IllegalStateException(IOS_UNAVAILABLE_MESSAGE)) + ): Result = runCatching { + FirebaseExternalUpdates.fcmToken.value + ?: throw IllegalStateException(TOKEN_UNAVAILABLE_MESSAGE) + } actual suspend fun getFirebaseInstallationId( context: PlatformContext, - ): Result = - Result.failure(IllegalStateException(IOS_UNAVAILABLE_MESSAGE)) + ): Result = runCatching { + FirebaseExternalUpdates.installationId.value + ?: throw IllegalStateException(INSTALLATION_ID_UNAVAILABLE_MESSAGE) + } } diff --git a/module/logging/logging/src/androidMain/kotlin/ru/bartwell/kick/module/logging/core/persist/DatabaseBuilder.kt b/module/logging/logging/src/androidMain/kotlin/ru/bartwell/kick/module/logging/core/persist/DatabaseBuilder.kt index 9df3e67a..2031d160 100644 --- a/module/logging/logging/src/androidMain/kotlin/ru/bartwell/kick/module/logging/core/persist/DatabaseBuilder.kt +++ b/module/logging/logging/src/androidMain/kotlin/ru/bartwell/kick/module/logging/core/persist/DatabaseBuilder.kt @@ -17,9 +17,6 @@ internal actual class DatabaseBuilder { context = appContext, name = "kick_logging.db" ) - try { - LoggingDb.Schema.synchronous().create(driver) - } catch (_: RuntimeException) {} val db = LoggingDb( driver = driver, logAdapter = Log.Adapter(levelAdapter = logLevelAdapter) diff --git a/module/network/ktor3/src/androidMain/kotlin/ru/bartwell/kick/module/ktor3/core/persist/DatabaseBuilder.kt b/module/network/ktor3/src/androidMain/kotlin/ru/bartwell/kick/module/ktor3/core/persist/DatabaseBuilder.kt index b4002451..b7c06b2d 100644 --- a/module/network/ktor3/src/androidMain/kotlin/ru/bartwell/kick/module/ktor3/core/persist/DatabaseBuilder.kt +++ b/module/network/ktor3/src/androidMain/kotlin/ru/bartwell/kick/module/ktor3/core/persist/DatabaseBuilder.kt @@ -17,9 +17,6 @@ internal actual class DatabaseBuilder { context = appContext, name = "kick_ktor3.db" ) - try { - Ktor3Db.Schema.synchronous().create(driver) - } catch (_: RuntimeException) {} val db = Ktor3Db( driver = driver, requestAdapter = Request.Adapter( From b26159f96819837bcc3bf7046482c3c4e9e272e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Mon, 16 Feb 2026 18:09:34 +0300 Subject: [PATCH 10/10] Minor fixes in Runner module --- .../runner/core/renderer/ObjectRunnerRenderer.kt | 11 +++++++---- .../params/presentation/RunnerParamsContent.kt | 11 +++++++---- .../kick/sample/shared/runner/RunnerSamples.kt | 10 +++++++++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/core/renderer/ObjectRunnerRenderer.kt b/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/core/renderer/ObjectRunnerRenderer.kt index 8c7607fc..b187da5d 100644 --- a/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/core/renderer/ObjectRunnerRenderer.kt +++ b/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/core/renderer/ObjectRunnerRenderer.kt @@ -3,13 +3,14 @@ package ru.bartwell.kick.module.runner.core.renderer import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ru.bartwell.kick.module.runner.core.data.RunnerRenderer @@ -26,15 +27,17 @@ public class ObjectRunnerRenderer : RunnerRenderer { @Composable override fun RenderContent(modifier: Modifier) { + val scrollState = rememberScrollState() val text = value?.toString() ?: "null" Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(16.dp), ) { Text( text = text, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp), ) } } diff --git a/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/feature/params/presentation/RunnerParamsContent.kt b/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/feature/params/presentation/RunnerParamsContent.kt index 99674387..f75a1b9c 100644 --- a/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/feature/params/presentation/RunnerParamsContent.kt +++ b/module/logging/runner/src/commonMain/kotlin/ru/bartwell/kick/module/runner/feature/params/presentation/RunnerParamsContent.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.Button import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -41,6 +40,7 @@ import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.subscribeAsState import ru.bartwell.kick.core.presentation.BackOrCloseButton import ru.bartwell.kick.core.presentation.ErrorBox +import ru.bartwell.kick.core.ui.ExposedDropdownMenuBox import ru.bartwell.kick.module.runner.core.params.RunnerParameter import ru.bartwell.kick.module.runner.core.params.RunnerParameterType @@ -248,7 +248,11 @@ private fun SingleChoiceParam( var expanded by remember { mutableStateOf(false) } val items = type.options val selected = value ?: param.defaultValue ?: items.firstOrNull() - Column { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = Modifier.fillMaxWidth(), + ) { OutlinedTextField( value = selected?.toString() ?: "", onValueChange = { }, @@ -264,8 +268,7 @@ private fun SingleChoiceParam( }, modifier = Modifier .fillMaxWidth() - .padding(bottom = 4.dp) - .clickable { expanded = true }, + .menuAnchor(), trailingIcon = { Icon(Icons.Outlined.Check, contentDescription = null) } diff --git a/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/runner/RunnerSamples.kt b/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/runner/RunnerSamples.kt index 52be17ec..7a7c377f 100644 --- a/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/runner/RunnerSamples.kt +++ b/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/runner/RunnerSamples.kt @@ -61,13 +61,21 @@ internal fun registerRunnerSamples() { type = RunnerParameterType.MultiChoice(options = listOf("A", "B", "C")), defaultValue = setOf("A"), ), + RunnerParameter( + id = "option", + title = "Option", + description = "Select one", + type = RunnerParameterType.SingleChoice(options = listOf("One", "Two", "Three")), + defaultValue = "One", + ), ), renderer = ObjectRunnerRenderer(), ) { args -> val count: Int = args.get("count") ?: 0 val label: String = args.get("label") ?: "" val flags: Set = args.get("flags") ?: emptySet() - "label=$label count=$count flags=${flags.joinToString()}" + val option: String = args.get("option") ?: "" + "label=$label count=$count flags=${flags.joinToString()} option=$option" } }