Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cmp-firebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`
Expand Down
86 changes: 62 additions & 24 deletions cmp-firebase/api/jvm/cmp-firebase.api
Original file line number Diff line number Diff line change
@@ -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 <init> ()V
public fun <init> (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 <init> (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 <init> ()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 <init> (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 <init> (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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 <init> (Ldev/gitlive/firebase/analytics/FirebaseAnalytics;Ljava/lang/String;)V
public synthetic fun <init> (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;
Expand Down
10 changes: 7 additions & 3 deletions cmp-firebase/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ kotlin {
val firebaseMain by creating {
dependsOn(commonMain.get())
dependencies {
api(libs.gitlive.firebase.app)
api(libs.gitlive.firebase.analytics)
}
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
}
Loading
Loading