diff --git a/cmp-firebase/README.md b/cmp-firebase/README.md index 86aa911..b911108 100644 --- a/cmp-firebase/README.md +++ b/cmp-firebase/README.md @@ -111,6 +111,37 @@ GitLive Firebase Analytics is brought in transitively as `api` on supported plat ## Setup +### One commonMain init (no native config files) + +The entire Firebase setup for **every platform** can be a single `commonMain` +call — no `google-services.json`, no `GoogleService-Info.plist`, no Swift +`FirebaseApp.configure()` line. Pass one `FirebaseConfig` holding each platform's +keys; the library selects the running platform, initializes Firebase +programmatically on the GitLive-native tier (Android/iOS/macOS/tvOS/JS) and wires +the Measurement-Protocol transport on the fallback tier (JVM/Linux/Windows/wasm): + +```kotlin +// commonMain — runs on every target +FirebaseKit.initialize( + FirebaseConfig( + android = FirebaseOptions(applicationId = "1:123:android:abc", apiKey = "AIza…", projectId = "my-proj"), + apple = FirebaseOptions(applicationId = "1:123:ios:def", apiKey = "AIza…", gcmSenderId = "123"), // ios/macos/tvos + web = FirebaseOptions(applicationId = "1:123:web:ghi", apiKey = "AIza…", authDomain = "my-proj.firebaseapp.com"), + measurementProtocol = MpConfig("G-XXXX", apiSecret = secureStore.read("MP_API_SECRET")), + ), +) +``` + +Per-platform notes: Apple's native `FIROptions` requires `gcmSenderId`; a platform +with no options degrades to a NoOp analytics helper with a WARN (it never throws). +Firebase `apiKey`/`applicationId`/`projectId` are client identifiers (safe in +source); the Measurement-Protocol `apiSecret` is the only secret — load it from +your secrets store, never hard-code it. On Android the required `Context` is +captured internally by `FirebaseInitProvider`, so the call stays 100% commonMain. + +The native-config-file paths below still work (the no-arg `FirebaseKit.initialize()` +keeps the legacy Android auto-init behavior) — use whichever fits your app. + ### Android 1. Add `google-services.json` to `app/` diff --git a/cmp-firebase/api/jvm/cmp-firebase.api b/cmp-firebase/api/jvm/cmp-firebase.api index 8616d05..efa2e2e 100644 --- a/cmp-firebase/api/jvm/cmp-firebase.api +++ b/cmp-firebase/api/jvm/cmp-firebase.api @@ -1,10 +1,71 @@ +public final class io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig { + public static final field Companion Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Companion; + public fun ()V + public fun (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig;)V + public synthetic fun (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public final fun component2 ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public final fun component3 ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public final fun component4 ()Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig; + public final fun copy (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig; + public static synthetic fun copy$default (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig;ILjava/lang/Object;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getAndroid ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public final fun getApple ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public final fun getMeasurementProtocol ()Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig; + public final fun getWeb ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public fun hashCode ()I + public final fun optionsForCurrentPlatform ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public fun toString ()Ljava/lang/String; +} + +public final class io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Builder { + public fun ()V + public final fun android (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Builder; + public final fun apple (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Builder; + public final fun build ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig; + public final fun measurementProtocol (Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/mp/MpConfig;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Builder; + public final fun web (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Builder; +} + +public final class io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Companion { + public final fun builder ()Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig$Builder; +} + public final class io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseKit { public static final field INSTANCE Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseKit; public final fun getCrashReporter ()Lio/github/mobilebytelabs/kmptoolkit/firebase/crashlytics/CrashReporter; public final fun initialize ()V + public final fun initialize (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig;)V public final fun isInitialized ()Z } +public final class io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public static synthetic fun copy$default (Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getApiKey ()Ljava/lang/String; + public final fun getApplicationId ()Ljava/lang/String; + public final fun getAuthDomain ()Ljava/lang/String; + public final fun getDatabaseUrl ()Ljava/lang/String; + public final fun getGaTrackingId ()Ljava/lang/String; + public final fun getGcmSenderId ()Ljava/lang/String; + public final fun getProjectId ()Ljava/lang/String; + public final fun getStorageBucket ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsBatch { public final fun add (Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsEvent;)Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsBatch; public final fun add (Ljava/lang/String;[Lkotlin/Pair;)Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsBatch; @@ -115,7 +176,7 @@ public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/Analyt public static fun setUserProperty (Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsHelper;Ljava/lang/String;Ljava/lang/String;)V } -public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider_firebaseKt { +public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider_nonfirebaseKt { public static final fun provideAnalyticsHelper ()Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsHelper; } @@ -224,29 +285,6 @@ public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/EventV public fun toString ()Ljava/lang/String; } -public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/FirebaseAnalyticsHelper : io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsHelper { - public static final field MAX_EVENT_NAME_LEN I - public static final field MAX_PARAM_KEY_LEN I - public static final field MAX_PARAM_VALUE_LEN I - public static final field MAX_USER_ID_LEN I - public static final field MAX_USER_PROP_NAME_LEN I - public static final field MAX_USER_PROP_VALUE_LEN I - public fun (Ldev/gitlive/firebase/analytics/FirebaseAnalytics;Ljava/lang/String;)V - public synthetic fun (Ldev/gitlive/firebase/analytics/FirebaseAnalytics;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun logButtonClick (Ljava/lang/String;Ljava/lang/String;)V - public fun logError (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public fun logEvent (Lio/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsEvent;)V - public fun logEvent (Ljava/lang/String;Ljava/util/Map;)V - public fun logEvent (Ljava/lang/String;[Lkotlin/Pair;)V - public fun logFeatureUsed (Ljava/lang/String;Ljava/lang/String;)V - public fun logScreenView (Ljava/lang/String;Ljava/lang/String;)V - public fun logStateTransition (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public fun setCollectionEnabled (Z)V - public fun setConsent (ZZ)V - public fun setUserId (Ljava/lang/String;)V - public fun setUserProperty (Ljava/lang/String;Ljava/lang/String;)V -} - public final class io/github/mobilebytelabs/kmptoolkit/firebase/analytics/Funnel { public static final field EVENT_ABANDON Ljava/lang/String; public static final field EVENT_COMPLETE Ljava/lang/String; diff --git a/cmp-firebase/build.gradle.kts b/cmp-firebase/build.gradle.kts index a82585e..6705c45 100644 --- a/cmp-firebase/build.gradle.kts +++ b/cmp-firebase/build.gradle.kts @@ -139,6 +139,7 @@ kotlin { val firebaseMain by creating { dependsOn(commonMain.get()) dependencies { + api(libs.gitlive.firebase.app) api(libs.gitlive.firebase.analytics) } } @@ -149,15 +150,18 @@ kotlin { dependsOn(commonMain.get()) } - // GitLive-supported → firebaseMain + // GitLive-supported native analytics + programmatic init → firebaseMain androidMain.get().dependsOn(firebaseMain) - jvmMain.get().dependsOn(firebaseMain) iosMain.get().dependsOn(firebaseMain) macosMain.get().dependsOn(firebaseMain) tvosMain.get().dependsOn(firebaseMain) jsMain.get().dependsOn(firebaseMain) - // Not supported by GitLive → nonFirebaseMain (Measurement Protocol HTTP) + // Not supported by GitLive → nonFirebaseMain (Measurement Protocol HTTP). + // JVM is here too: GitLive's JVM analytics is a no-op stub AND JVM has no + // Android Context for programmatic init, so JVM runs on the MP tier + // (no-op platformInitializeFirebase + MP analytics). + jvmMain.get().dependsOn(nonFirebaseMain) linuxMain.get().dependsOn(nonFirebaseMain) mingwMain.get().dependsOn(nonFirebaseMain) wasmJsMain.get().dependsOn(nonFirebaseMain) diff --git a/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/AndroidFirebaseContext.kt b/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/AndroidFirebaseContext.kt new file mode 100644 index 0000000..fcbdf19 --- /dev/null +++ b/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/AndroidFirebaseContext.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +import android.content.Context + +/** + * Android application-Context holder for programmatic Firebase init. + * + * `FirebaseInitProvider` (a `ContentProvider`, runs before `Application.onCreate`) + * captures the application `Context` here, so `FirebaseKit.initialize(config)` + * stays a pure commonMain call — the consumer never passes a Context. + * + * If the provider is removed from the merged manifest (`tools:node="remove"`), + * call [setApplicationContext] manually before `FirebaseKit.initialize(config)`. + */ +public object AndroidFirebaseContext { + + /** The captured application [Context], or `null` if not yet set. */ + val app: Context? + get() = FirebaseNativeContext.value as? Context + + /** Manually supply the application [Context] (provider-removed fallback). */ + public fun setApplicationContext(context: Context) { + FirebaseNativeContext.value = context.applicationContext + } +} diff --git a/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseInitProvider.kt b/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseInitProvider.kt index 19070c1..02a6ab3 100644 --- a/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseInitProvider.kt +++ b/cmp-firebase/src/androidMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseInitProvider.kt @@ -29,7 +29,11 @@ import android.net.Uri internal class FirebaseInitProvider : ContentProvider() { override fun onCreate(): Boolean { - runCatching { FirebaseKit.initialize() } + // Capture the application Context (runs before Application.onCreate) so the + // consumer's commonMain FirebaseKit.initialize(config) can pass it to GitLive's + // Firebase.initialize on Android. Does NOT initialize Firebase itself — that is + // the consumer's single commonMain init call. + runCatching { context?.applicationContext?.let { AndroidFirebaseContext.setApplicationContext(it) } } return true } diff --git a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig.kt b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig.kt new file mode 100644 index 0000000..8a15e1f --- /dev/null +++ b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfig.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.kmpPlatform +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MpConfig + +/** + * The single, commonMain Firebase configuration for [FirebaseKit.initialize]. + * + * One object carries every platform's [FirebaseOptions]. At init, the library + * selects the entry matching the running platform (via [kmpPlatform]) and + * initializes Firebase programmatically — no `google-services.json`, + * `GoogleService-Info.plist`, or Swift `FirebaseApp.configure()` line required. + * + * ### Tier routing + * - **Native (GitLive)** — `android`, `apple` (ios/macos/tvos), `web` (js) → + * `Firebase.initialize(options)`. + * - **Measurement-Protocol** — jvm / watchos / linux / windows / wasmjs have no + * native Firebase (or a stub); analytics flows through [measurementProtocol]. + * Leave [measurementProtocol] null to no-op analytics on those targets. + * + * Apple is ONE grouped entry: iOS, macOS and tvOS share [apple]. A platform with + * no matching options degrades to a no-op init + NoOp analytics (never throws). + * + * ### Usage + * ```kotlin + * FirebaseKit.initialize( + * FirebaseConfig( + * android = FirebaseOptions("1:123:android:abc", "AIza…", projectId = "my-proj"), + * apple = FirebaseOptions("1:123:ios:def", "AIza…", gcmSenderId = "123"), + * web = FirebaseOptions("1:123:web:ghi", "AIza…", authDomain = "my-proj.firebaseapp.com"), + * measurementProtocol = MpConfig("G-XXXX", apiSecret = secureStore.read("MP_API_SECRET")), + * ), + * ) + * ``` + */ +public data class FirebaseConfig( + val android: FirebaseOptions? = null, + /** iOS + macOS + tvOS (one grouped entry). */ + val apple: FirebaseOptions? = null, + /** Web (js). */ + val web: FirebaseOptions? = null, + /** Measurement-Protocol transport for the non-GitLive tier. */ + val measurementProtocol: MpConfig? = null, +) { + /** + * Pure, unit-testable selector: the [FirebaseOptions] for a given [kmpPlatform] + * string, or `null` when this platform is on the Measurement-Protocol tier + * (or no options were supplied). + */ + internal fun optionsForPlatform(platform: String): FirebaseOptions? = when (platform) { + "android" -> android + "ios", "macos", "tvos" -> apple + "js" -> web + else -> null // jvm / watchos / linux / mingw / wasmjs → MP tier, no native options + } + + /** The [FirebaseOptions] for the platform this code is running on. */ + public fun optionsForCurrentPlatform(): FirebaseOptions? = optionsForPlatform(kmpPlatform) + + /** Fluent builder — mirrors the toolkit's `AppUpdateConfig` DSL. */ + public class Builder { + private var android: FirebaseOptions? = null + private var apple: FirebaseOptions? = null + private var web: FirebaseOptions? = null + private var mp: MpConfig? = null + + public fun android(options: FirebaseOptions): Builder = apply { this.android = options } + public fun apple(options: FirebaseOptions): Builder = apply { this.apple = options } + public fun web(options: FirebaseOptions): Builder = apply { this.web = options } + public fun measurementProtocol(config: MpConfig): Builder = apply { this.mp = config } + + public fun build(): FirebaseConfig = FirebaseConfig(android, apple, web, mp) + } + + public companion object { + public fun builder(): Builder = Builder() + } +} diff --git a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseKit.kt b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseKit.kt index e4d93e7..ae8beab 100644 --- a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseKit.kt +++ b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseKit.kt @@ -9,6 +9,8 @@ */ package io.github.mobilebytelabs.kmptoolkit.firebase +import co.touchlab.kermit.Logger +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.kmpPlatform import io.github.mobilebytelabs.kmptoolkit.firebase.crashlytics.CrashReporter import io.github.mobilebytelabs.kmptoolkit.firebase.crashlytics.provideCrashReporter @@ -68,4 +70,29 @@ object FirebaseKit { crashReporter.install() initialized = true } + + /** + * Fully-commonMain programmatic init from one per-platform [config]. + * + * Idempotent. Stashes [config] (so the Measurement-Protocol analytics factory + * can auto-wire from it), initializes native Firebase for the running platform + * where GitLive supports it, then installs the crash reporter. When this + * platform has no options (and is not on the MP tier), analytics degrades to + * NoOp with a WARN — it never throws (analytics must not break the app). + */ + fun initialize(config: FirebaseConfig) { + if (initialized) return + FirebaseRuntime.config = config + val options = config.optionsForCurrentPlatform() + if (options == null && config.measurementProtocol == null) { + Logger.w(TAG) { + "No Firebase options for platform '$kmpPlatform' and no MpConfig — analytics will NoOp." + } + } + platformInitializeFirebase(options) + crashReporter.install() + initialized = true + } + + private const val TAG = "FirebaseKit" } diff --git a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions.kt b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions.kt new file mode 100644 index 0000000..60b4493 --- /dev/null +++ b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseOptions.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +/** + * Platform-agnostic Firebase project identifiers — the common superset of the + * fields GitLive's native `FirebaseOptions` needs across Android / Apple / Web. + * + * These are **client identifiers, not secrets** (the same values shipped inside + * `google-services.json` / `GoogleService-Info.plist`); they are safe to hold in + * commonMain source. The genuinely-sensitive Measurement-Protocol `apiSecret` + * lives on [io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MpConfig], + * not here, and must be loaded from a secrets store at runtime. + * + * ### Per-platform required fields (Firebase constraints) + * - **Android**: `applicationId` + `apiKey` (+ `projectId` recommended). + * - **Apple (iOS/macOS/tvOS)**: `applicationId` + `apiKey` + **`gcmSenderId`** + * (the native `FIROptions` constructor requires GCMSenderID). `gaTrackingId` + * is ignored on Apple. + * - **Web (JS)**: `applicationId` + `apiKey` + `projectId`; `authDomain` is + * Auth-specific. (GitLive's web options cannot carry a GA4 `measurementId`.) + * + * @property applicationId the per-platform Firebase App ID (a.k.a. googleAppID). Required. + * @property apiKey the Firebase API key. Required. + * @property projectId the Firebase/GCP project id. + * @property gcmSenderId messaging sender id; required on Apple. + * @property storageBucket Cloud Storage bucket. + * @property databaseUrl Realtime Database URL. + * @property authDomain web Auth domain (JS only). + * @property gaTrackingId legacy GA tracking id (dropped on Apple). + */ +public data class FirebaseOptions( + val applicationId: String, + val apiKey: String, + val projectId: String? = null, + val gcmSenderId: String? = null, + val storageBucket: String? = null, + val databaseUrl: String? = null, + val authDomain: String? = null, + val gaTrackingId: String? = null, +) { + init { + require(applicationId.isNotBlank()) { "FirebaseOptions.applicationId cannot be blank" } + require(apiKey.isNotBlank()) { "FirebaseOptions.apiKey cannot be blank" } + } +} diff --git a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseRuntime.kt b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseRuntime.kt new file mode 100644 index 0000000..b7f0579 --- /dev/null +++ b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseRuntime.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +import kotlin.concurrent.Volatile + +/** + * Internal holder for the active [FirebaseConfig] set by [FirebaseKit.initialize]. + * + * The Measurement-Protocol analytics factory (`provideAnalyticsHelper()` on the + * non-GitLive tier) reads [config] to auto-wire the MP helper from the same + * single config the consumer passed at init — no separate per-platform DI. + */ +internal object FirebaseRuntime { + @Volatile + var config: FirebaseConfig? = null +} + +/** + * Platform-init Context carrier, typed [Any] so it lives in commonMain. + * + * On Android, `FirebaseInitProvider` (a ContentProvider that runs before + * `Application.onCreate`) writes the application `Context` here; GitLive's + * `Firebase.initialize(context, options)` requires it on Android. On every other + * platform it stays `null` (GitLive ignores the argument). This keeps the + * consumer's `FirebaseKit.initialize(config)` call 100% commonMain. + */ +internal object FirebaseNativeContext { + @Volatile + var value: Any? = null +} diff --git a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.kt b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.kt new file mode 100644 index 0000000..6958573 --- /dev/null +++ b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +/** + * Programmatically initialize the platform's native Firebase from [options]. + * + * - **GitLive tier** (android/ios/macos/tvos/js) → `Firebase.initialize(options)`. + * - **Measurement-Protocol tier** (jvm/watchos/linux/mingw/wasmjs) → no-op + * (no native Firebase SDK; analytics flows through the MP helper instead). + * + * A null [options] (no keys supplied for this platform) is a no-op on every + * target — the caller has already logged the graceful-degradation warning. + */ +internal expect fun platformInitializeFirebase(options: FirebaseOptions?) diff --git a/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/InMemorySettings.kt b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/InMemorySettings.kt new file mode 100644 index 0000000..8fa5282 --- /dev/null +++ b/cmp-firebase/src/commonMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/InMemorySettings.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase.analytics + +import com.russhwolf.settings.Settings + +/** + * Zero-dependency in-memory [Settings], used as the default backing store for the + * auto-wired Measurement-Protocol helper on the fallback tier + * (jvm / linux / mingw / wasmjs). + * + * It compiles on every KMP target (multiplatform-settings ships no common + * persistent implementation, and `MapSettings` is test-only). The MP client-id is + * therefore ephemeral per process on these targets — an already-documented + * limitation of the Measurement-Protocol tier. Apps that need persistence + * construct the MP helper themselves with a platform [Settings]. + */ +internal class InMemorySettings : Settings { + private val map = mutableMapOf() + + override val keys: Set get() = map.keys.toSet() + override val size: Int get() = map.size + override fun clear() = map.clear() + override fun remove(key: String) { map.remove(key) } + override fun hasKey(key: String): Boolean = map.containsKey(key) + + override fun putInt(key: String, value: Int) { map[key] = value } + override fun getInt(key: String, defaultValue: Int): Int = map[key] as? Int ?: defaultValue + override fun getIntOrNull(key: String): Int? = map[key] as? Int + + override fun putLong(key: String, value: Long) { map[key] = value } + override fun getLong(key: String, defaultValue: Long): Long = map[key] as? Long ?: defaultValue + override fun getLongOrNull(key: String): Long? = map[key] as? Long + + override fun putString(key: String, value: String) { map[key] = value } + override fun getString(key: String, defaultValue: String): String = map[key] as? String ?: defaultValue + override fun getStringOrNull(key: String): String? = map[key] as? String + + override fun putFloat(key: String, value: Float) { map[key] = value } + override fun getFloat(key: String, defaultValue: Float): Float = map[key] as? Float ?: defaultValue + override fun getFloatOrNull(key: String): Float? = map[key] as? Float + + override fun putDouble(key: String, value: Double) { map[key] = value } + override fun getDouble(key: String, defaultValue: Double): Double = map[key] as? Double ?: defaultValue + override fun getDoubleOrNull(key: String): Double? = map[key] as? Double + + override fun putBoolean(key: String, value: Boolean) { map[key] = value } + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = map[key] as? Boolean ?: defaultValue + override fun getBooleanOrNull(key: String): Boolean? = map[key] as? Boolean +} diff --git a/cmp-firebase/src/firebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.firebase.kt b/cmp-firebase/src/firebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.firebase.kt new file mode 100644 index 0000000..b7286ab --- /dev/null +++ b/cmp-firebase/src/firebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.firebase.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.initialize +import dev.gitlive.firebase.FirebaseOptions as GitLiveFirebaseOptions + +/** + * Map the common [FirebaseOptions] superset to GitLive's native `FirebaseOptions`. + * + * Per-platform notes: Apple's native `FIROptions` constructor requires + * `gcmSenderId` (supply it in [FirebaseOptions.gcmSenderId]); `gaTrackingId` is + * dropped on Apple by GitLive. Web reads `authDomain`. + */ +internal fun FirebaseOptions.toGitLive(): GitLiveFirebaseOptions = GitLiveFirebaseOptions( + applicationId = applicationId, + apiKey = apiKey, + databaseUrl = databaseUrl, + gaTrackingId = gaTrackingId, + storageBucket = storageBucket, + projectId = projectId, + gcmSenderId = gcmSenderId, + authDomain = authDomain, +) + +/** + * GitLive-native tier (android / ios / macos / tvos / js): configure the default + * FirebaseApp programmatically. On Android the required `Context` is read from + * [FirebaseNativeContext] (captured by `FirebaseInitProvider`); elsewhere it is + * `null` and GitLive ignores it. + */ +internal actual fun platformInitializeFirebase(options: FirebaseOptions?) { + val opts = options ?: return + Firebase.initialize(context = FirebaseNativeContext.value, options = opts.toGitLive()) +} diff --git a/cmp-firebase/src/jvmTest/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfigTest.kt b/cmp-firebase/src/jvmTest/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfigTest.kt new file mode 100644 index 0000000..5475f64 --- /dev/null +++ b/cmp-firebase/src/jvmTest/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/FirebaseConfigTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +/** + * Covers the pure per-platform selection seam, idempotency, and graceful + * degradation. Runs on JVM (the Measurement-Protocol tier), where + * `platformInitializeFirebase` is a no-op — so `FirebaseKit.initialize(config)` + * exercises the real control flow without touching a native Firebase SDK. + */ +class FirebaseConfigTest { + + private val cfg = FirebaseConfig( + android = FirebaseOptions(applicationId = "1:1:android:a", apiKey = "k-android"), + apple = FirebaseOptions(applicationId = "1:1:ios:b", apiKey = "k-apple", gcmSenderId = "1"), + web = FirebaseOptions(applicationId = "1:1:web:c", apiKey = "k-web"), + ) + + @Test + fun android_web_and_apple_group_select_correctly() { + assertSame(cfg.android, cfg.optionsForPlatform("android")) + assertSame(cfg.web, cfg.optionsForPlatform("js")) + // Apple is ONE grouped entry for ios/macos/tvos. + assertSame(cfg.apple, cfg.optionsForPlatform("ios")) + assertSame(cfg.apple, cfg.optionsForPlatform("macos")) + assertSame(cfg.apple, cfg.optionsForPlatform("tvos")) + } + + @Test + fun fallback_tier_platforms_have_no_native_options() { + listOf("jvm", "linux", "mingw", "wasmjs", "watchos").forEach { + assertNull(cfg.optionsForPlatform(it), "expected null native options for $it") + } + } + + @Test + fun missing_keys_degrade_gracefully_without_throwing() { + val empty = FirebaseConfig() + assertNull(empty.optionsForCurrentPlatform()) + // No native options + no throw — the graceful-degradation contract. + platformInitializeFirebase(empty.optionsForCurrentPlatform()) + FirebaseKit.initialize(empty) + assertTrue(FirebaseKit.isInitialized) + } + + @Test + fun initialize_is_idempotent() { + FirebaseKit.initialize(cfg) + assertTrue(FirebaseKit.isInitialized) + // Second call is a safe no-op — never throws, state stays initialized. + FirebaseKit.initialize(cfg) + assertTrue(FirebaseKit.isInitialized) + } + + @Test + fun builder_produces_equivalent_config() { + val built = FirebaseConfig.builder() + .android(cfg.android!!) + .apple(cfg.apple!!) + .web(cfg.web!!) + .build() + assertNotNull(built.optionsForPlatform("android")) + assertSame(cfg.apple, built.optionsForPlatform("ios")) + } +} diff --git a/cmp-firebase/src/jvmTest/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsTierRoutingTest.kt b/cmp-firebase/src/jvmTest/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsTierRoutingTest.kt new file mode 100644 index 0000000..85f1e1e --- /dev/null +++ b/cmp-firebase/src/jvmTest/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsTierRoutingTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase.analytics + +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseConfig +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseRuntime +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MeasurementProtocolAnalyticsHelper +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MpConfig +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertSame +import kotlin.test.assertTrue + +/** + * The Measurement-Protocol tier's `provideAnalyticsHelper()` must auto-wire the + * MP helper from the stashed [FirebaseConfig] when a [MpConfig] is present, and + * fall back to [NoOpAnalyticsHelper] otherwise. Runs on JVM (the MP tier). + */ +class AnalyticsTierRoutingTest { + + @AfterTest + fun reset() { + FirebaseRuntime.config = null + } + + @Test + fun mp_helper_when_measurement_protocol_present() { + FirebaseRuntime.config = FirebaseConfig( + measurementProtocol = MpConfig(measurementId = "G-TEST", apiSecret = "secret"), + ) + assertTrue(provideAnalyticsHelper() is MeasurementProtocolAnalyticsHelper) + } + + @Test + fun noop_when_measurement_protocol_absent() { + FirebaseRuntime.config = FirebaseConfig() + assertSame(NoOpAnalyticsHelper, provideAnalyticsHelper()) + } + + @Test + fun noop_when_no_config_at_all() { + FirebaseRuntime.config = null + assertSame(NoOpAnalyticsHelper, provideAnalyticsHelper()) + } +} diff --git a/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.nonfirebase.kt b/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.nonfirebase.kt new file mode 100644 index 0000000..0df99bc --- /dev/null +++ b/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/PlatformInit.nonfirebase.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2026 MobileByteLabs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.mobilebytelabs.kmptoolkit.firebase + +/** + * Measurement-Protocol / fallback tier (jvm / linux / mingw / wasmjs): no native + * Firebase SDK ships here, so init is a no-op. Analytics on these targets flows + * through the Measurement-Protocol helper, auto-wired from the same + * [FirebaseConfig] by `provideAnalyticsHelper()`. + */ +internal actual fun platformInitializeFirebase(options: FirebaseOptions?) { + // No native Firebase on this tier; MP transport is wired by provideAnalyticsHelper(). +} diff --git a/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider.nonfirebase.kt b/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider.nonfirebase.kt index 7fef2a1..38ea0a3 100644 --- a/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider.nonfirebase.kt +++ b/cmp-firebase/src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider.nonfirebase.kt @@ -9,6 +9,9 @@ */ package io.github.mobilebytelabs.kmptoolkit.firebase.analytics +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseRuntime +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MeasurementProtocolAnalyticsHelper + /** * Non-Firebase tier actual: watchOS (×4) · Linux (×2) · mingwX64 · wasmJs · wasmWasi. * @@ -42,4 +45,7 @@ package io.github.mobilebytelabs.kmptoolkit.firebase.analytics * } * ``` */ -actual fun provideAnalyticsHelper(): AnalyticsHelper = NoOpAnalyticsHelper +actual fun provideAnalyticsHelper(): AnalyticsHelper = + FirebaseRuntime.config?.measurementProtocol + ?.let { MeasurementProtocolAnalyticsHelper(config = it, settings = InMemorySettings()) } + ?: NoOpAnalyticsHelper diff --git a/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/green/AnalyticsProvider.nonfirebase.kt b/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/green/AnalyticsProvider.nonfirebase.kt new file mode 100644 index 0000000..a865253 --- /dev/null +++ b/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/green/AnalyticsProvider.nonfirebase.kt @@ -0,0 +1,12 @@ +// GREEN fixture — the FIX: the Measurement-Protocol tier auto-wires the MP helper +// from the stashed FirebaseConfig when a MpConfig is present, else NoOp. One +// commonMain FirebaseConfig drives both tiers; events flow everywhere keys exist. +package io.github.mobilebytelabs.kmptoolkit.firebase.analytics + +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseRuntime +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MeasurementProtocolAnalyticsHelper + +actual fun provideAnalyticsHelper(): AnalyticsHelper = + FirebaseRuntime.config?.measurementProtocol + ?.let { MeasurementProtocolAnalyticsHelper(config = it, settings = InMemorySettings()) } + ?: NoOpAnalyticsHelper diff --git a/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/red/AnalyticsProvider.nonfirebase.kt b/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/red/AnalyticsProvider.nonfirebase.kt new file mode 100644 index 0000000..418cdf6 --- /dev/null +++ b/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/red/AnalyticsProvider.nonfirebase.kt @@ -0,0 +1,7 @@ +// RED fixture — the PRE-FIX bug: the Measurement-Protocol tier hard-returns NoOp +// and ignores the stashed FirebaseConfig, so events never flow on jvm/desktop/web +// even when the consumer supplied an MpConfig. The canary asserts this fixture +// does NOT read the MP config field (proving the guard catches the regression). +package io.github.mobilebytelabs.kmptoolkit.firebase.analytics + +actual fun provideAnalyticsHelper(): AnalyticsHelper = NoOpAnalyticsHelper diff --git a/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/run.sh b/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/run.sh new file mode 100755 index 0000000..a71f081 --- /dev/null +++ b/cmp-firebase/tests/fixtures/firebase-commonmain-init-canary/run.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Canary — proves the MP-tier auto-wire guard for cmp-firebase commonMain init. +# GREEN (config-reading actual) must read `measurementProtocol`; RED (hard-NoOp) +# must NOT. A future regression that reverts the actual to hard-NoOp fails here. +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" + +green="$DIR/green/AnalyticsProvider.nonfirebase.kt" +red="$DIR/red/AnalyticsProvider.nonfirebase.kt" + +if ! grep -q "measurementProtocol" "$green"; then + echo "FAIL: GREEN fixture must read config.measurementProtocol (auto-wire)"; exit 1 +fi +if grep -q "measurementProtocol" "$red"; then + echo "FAIL: RED fixture must NOT read config.measurementProtocol (hard-NoOp bug)"; exit 1 +fi +# Cross-check the live source matches the GREEN contract. +live="$DIR/../../../src/nonFirebaseMain/kotlin/io/github/mobilebytelabs/kmptoolkit/firebase/analytics/AnalyticsProvider.nonfirebase.kt" +if [ -f "$live" ] && ! grep -q "measurementProtocol" "$live"; then + echo "FAIL: live nonFirebase provider regressed to hard-NoOp"; exit 1 +fi +echo "canary OK" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f0c7043..9c9b455 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -105,6 +105,7 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } # GitLive Firebase Kotlin SDK +gitlive-firebase-app = { module = "dev.gitlive:firebase-app", version.ref = "gitliveFirebase" } gitlive-firebase-analytics = { module = "dev.gitlive:firebase-analytics", version.ref = "gitliveFirebase" } gitlive-firebase-common = { module = "dev.gitlive:firebase-common", version.ref = "gitliveFirebase" } gitlive-firebase-crashlytics = { module = "dev.gitlive:firebase-crashlytics", version.ref = "gitliveFirebase" } diff --git a/samples/sample-toolkit/composeApp/src/commonMain/kotlin/com/mobilebytelabs/kmptoolkit/samples/toolkit/features/firebaseanalytics/FirebaseAnalyticsDemoScreen.kt b/samples/sample-toolkit/composeApp/src/commonMain/kotlin/com/mobilebytelabs/kmptoolkit/samples/toolkit/features/firebaseanalytics/FirebaseAnalyticsDemoScreen.kt index e303094..57a7a68 100644 --- a/samples/sample-toolkit/composeApp/src/commonMain/kotlin/com/mobilebytelabs/kmptoolkit/samples/toolkit/features/firebaseanalytics/FirebaseAnalyticsDemoScreen.kt +++ b/samples/sample-toolkit/composeApp/src/commonMain/kotlin/com/mobilebytelabs/kmptoolkit/samples/toolkit/features/firebaseanalytics/FirebaseAnalyticsDemoScreen.kt @@ -8,10 +8,14 @@ import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseConfig +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseKit +import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseOptions import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.AnalyticsEvent import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.AnalyticsHelper import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.Param import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.StubAnalyticsHelper +import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MpConfig import com.mobilebytelabs.kmptoolkit.samples.toolkit.features._shared.DemoIntro import com.mobilebytelabs.kmptoolkit.samples.toolkit.features._shared.SetupRequiredCard @@ -35,12 +39,28 @@ fun FirebaseAnalyticsDemoScreen(onStatus: (String) -> Unit) { ) { Text("Log AnalyticsEvent (stub)") } SetupRequiredCard( - title = "Wire a real backend for production", - explanation = "StubAnalyticsHelper is in-process — events never leave the app. Choose GitLive (mobile) or Measurement Protocol (desktop/web) for actual delivery.", + title = "One commonMain init for production", + explanation = "One call configures every platform — no google-services.json / GoogleService-Info.plist / Swift configure() line. See exampleCommonMainInit() below.", setupSteps = listOf( - "Mobile: createGitLiveAnalyticsHelper() — needs Firebase config", - "Desktop/Web/Native: MeasurementProtocolAnalyticsHelper(measurementId, apiSecret)", - "Register in DI: single AnalyticsHelper instance for the whole app", + "commonMain: FirebaseKit.initialize(FirebaseConfig(android=…, apple=…, web=…, measurementProtocol=…))", + "Native (Android/iOS/macOS/tvOS/JS): GitLive initializes Firebase from your keys", + "Fallback (JVM/desktop/web-wasm/native): Measurement Protocol via the same MpConfig", + ), + ) +} + +/** + * Reference: the entire Firebase setup for every platform is this one commonMain + * call. Firebase apiKey/appId/projectId are client identifiers (safe in source); + * the Measurement-Protocol apiSecret is loaded from a secrets store at runtime. + */ +fun exampleCommonMainInit(mpApiSecret: String) { + FirebaseKit.initialize( + FirebaseConfig( + android = FirebaseOptions(applicationId = "1:123:android:abc", apiKey = "AIza…", projectId = "demo"), + apple = FirebaseOptions(applicationId = "1:123:ios:def", apiKey = "AIza…", gcmSenderId = "123"), + web = FirebaseOptions(applicationId = "1:123:web:ghi", apiKey = "AIza…", authDomain = "demo.firebaseapp.com"), + measurementProtocol = MpConfig(measurementId = "G-DEMO", apiSecret = mpApiSecret), ), ) }