diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index a13529ec8..423a3291e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -28,6 +28,8 @@ jobs:
run: ./gradlew :updateVersions
- name: Grant execute permission for gradlew
run: chmod +x gradlew
+ - name: Publish Firebase Analytics
+ run: ./gradlew :firebase-analytics:publish
- name: Publish Firebase App
run: ./gradlew :firebase-app:publish
- name: Publish Firebase Auth
diff --git a/build.gradle.kts b/build.gradle.kts
index 507fb50f9..8004f17b4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -36,6 +36,7 @@ val minSdkVersion by extra(23)
tasks {
register("updateVersions") {
dependsOn(
+ "firebase-analytics:updateVersion", "firebase-analytics:updateDependencyVersion",
"firebase-app:updateVersion", "firebase-app:updateDependencyVersion",
"firebase-auth:updateVersion", "firebase-auth:updateDependencyVersion",
"firebase-common:updateVersion", "firebase-common:updateDependencyVersion",
diff --git a/firebase-analytics/build.gradle.kts b/firebase-analytics/build.gradle.kts
new file mode 100644
index 000000000..4d68ac2c8
--- /dev/null
+++ b/firebase-analytics/build.gradle.kts
@@ -0,0 +1,173 @@
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
+
+/*
+ * Copyright (c) 2023 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+version = project.property("firebase-analytics.version") as String
+
+plugins {
+ id("com.android.library")
+ kotlin("native.cocoapods")
+ kotlin("multiplatform")
+}
+
+android {
+ val minSdkVersion: Int by project
+ val compileSdkVersion: Int by project
+
+ compileSdk = compileSdkVersion
+ namespace = "dev.gitlive.firebase.analytics"
+
+ defaultConfig {
+ minSdk = minSdkVersion
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ testOptions {
+ unitTests.apply {
+ isIncludeAndroidResources = true
+ }
+ }
+ packaging {
+ resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
+ resources.pickFirsts.add("META-INF/AL2.0")
+ resources.pickFirsts.add("META-INF/LGPL2.1")
+ }
+ lint {
+ abortOnError = false
+ }
+}
+
+val supportIosTarget = project.property("skipIosTarget") != "true"
+
+kotlin {
+
+ targets.configureEach {
+ compilations.configureEach {
+ kotlinOptions.freeCompilerArgs += "-Xexpect-actual-classes"
+ }
+ }
+
+ @Suppress("OPT_IN_USAGE")
+ androidTarget {
+ instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
+ unitTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
+ publishAllLibraryVariants()
+ compilations.configureEach {
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ }
+ }
+
+ jvm {
+ compilations.getByName("main") {
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ }
+ compilations.getByName("test") {
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ }
+ }
+
+ if (supportIosTarget) {
+ iosArm64()
+ iosX64()
+ iosSimulatorArm64()
+ cocoapods {
+ ios.deploymentTarget = "12.0"
+ framework {
+ baseName = "FirebaseAnalytics"
+ }
+ noPodspec()
+ pod("FirebaseAnalytics") {
+ version = "10.25.0"
+ extraOpts += listOf("-compiler-option", "-fmodules")
+ }
+ }
+ }
+
+ js(IR) {
+ useCommonJs()
+ nodejs {
+ testTask(
+ Action {
+ useKarma {
+ useChromeHeadless()
+ }
+ }
+ )
+ }
+ browser {
+ testTask(
+ Action {
+ useKarma {
+ useChromeHeadless()
+ }
+ }
+ )
+ }
+ }
+
+ sourceSets {
+ all {
+ languageSettings.apply {
+ val apiVersion: String by project
+ val languageVersion: String by project
+ this.apiVersion = apiVersion
+ this.languageVersion = languageVersion
+ progressiveMode = true
+ if (name.lowercase().contains("ios")) {
+ optIn("kotlinx.cinterop.ExperimentalForeignApi")
+ }
+ }
+ }
+
+ getByName("commonMain") {
+ dependencies {
+ api(project(":firebase-app"))
+ implementation(project(":firebase-common"))
+ }
+ }
+
+ getByName("commonTest") {
+ dependencies {
+ implementation(project(":test-utils"))
+ }
+ }
+
+ getByName("androidMain") {
+ dependencies {
+ api("com.google.firebase:firebase-analytics")
+ }
+ }
+ }
+}
+
+if (project.property("firebase-analytics.skipIosTests") == "true") {
+ tasks.forEach {
+ if (it.name.contains("ios", true) && it.name.contains("test", true)) { it.enabled = false }
+ }
+}
+
+if (project.property("firebase-analytics.skipJsTests") == "true") {
+ tasks.forEach {
+ if (it.name.contains("js", true) && it.name.contains("test", true)) { it.enabled = false }
+ }
+}
+
+signing {
+ val signingKey: String? by project
+ val signingPassword: String? by project
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign(publishing.publications)
+}
diff --git a/firebase-analytics/firebase_analytics.podspec b/firebase-analytics/firebase_analytics.podspec
new file mode 100644
index 000000000..74c7cb847
--- /dev/null
+++ b/firebase-analytics/firebase_analytics.podspec
@@ -0,0 +1,39 @@
+Pod::Spec.new do |spec|
+ spec.name = 'firebase_analytics'
+ spec.version = '1.8.1'
+ spec.homepage = ''
+ spec.source = { :http=> ''}
+ spec.authors = ''
+ spec.license = ''
+ spec.summary = ''
+ spec.vendored_frameworks = 'build/cocoapods/framework/firebase_analytics.framework'
+ spec.libraries = 'c++'
+
+
+
+ spec.pod_target_xcconfig = {
+ 'KOTLIN_PROJECT_PATH' => ':firebase-analytics',
+ 'PRODUCT_MODULE_NAME' => 'firebase_analytics',
+ }
+
+ spec.script_phases = [
+ {
+ :name => 'Build firebase_analytics',
+ :execution_position => :before_compile,
+ :shell_path => '/bin/sh',
+ :script => <<-SCRIPT
+ if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
+ echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
+ exit 0
+ fi
+ set -ev
+ REPO_ROOT="$PODS_TARGET_SRCROOT"
+ "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
+ -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
+ -Pkotlin.native.cocoapods.archs="$ARCHS" \
+ -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
+ SCRIPT
+ }
+ ]
+
+end
diff --git a/firebase-analytics/package.json b/firebase-analytics/package.json
new file mode 100644
index 000000000..9736e8e39
--- /dev/null
+++ b/firebase-analytics/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@gitlive/firebase-analytics",
+ "version": "1.12.0",
+ "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects",
+ "main": "firebase-analytics.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/GitLiveApp/firebase-kotlin-sdk.git"
+ },
+ "keywords": [
+ "kotlin",
+ "multiplatform",
+ "kotlin-js",
+ "firebase"
+ ],
+ "author": "dev.gitlive",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/GitLiveApp/firebase-kotlin-sdk/issues"
+ },
+ "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk",
+ "dependencies": {
+ "@gitlive/firebase-app": "1.12.0",
+ "firebase": "9.19.1",
+ "kotlin": "1.6.10",
+ "kotlinx-coroutines-core": "1.6.1-native-mt"
+ }
+}
diff --git a/firebase-analytics/src/androidInstrumentedTest/AndroidManifest.xml b/firebase-analytics/src/androidInstrumentedTest/AndroidManifest.xml
new file mode 100644
index 000000000..3d8df4081
--- /dev/null
+++ b/firebase-analytics/src/androidInstrumentedTest/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/firebase-analytics/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..9b6ae269a
--- /dev/null
+++ b/firebase-analytics/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+@file:JvmName("tests")
+package dev.gitlive.firebase.analytics
+
+import androidx.test.platform.app.InstrumentationRegistry
+
+actual val emulatorHost: String = "10.0.2.2"
+
+actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+actual annotation class IgnoreForAndroidUnitTest
diff --git a/firebase-analytics/src/androidMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/androidMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..31795e294
--- /dev/null
+++ b/firebase-analytics/src/androidMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,103 @@
+@file:JvmName("analyticsAndroid")
+package dev.gitlive.firebase.analytics
+
+import android.os.Bundle
+import android.os.IBinder
+import android.os.Parcelable
+import android.util.Size
+import android.util.SizeF
+import com.google.firebase.analytics.analytics
+import com.google.firebase.analytics.setConsent
+import dev.gitlive.firebase.Firebase
+import dev.gitlive.firebase.FirebaseApp
+import kotlinx.coroutines.tasks.await
+import java.io.Serializable
+
+actual val Firebase.analytics: FirebaseAnalytics
+ get() = FirebaseAnalytics(com.google.firebase.Firebase.analytics)
+
+actual fun Firebase.analytics(app: FirebaseApp) =
+ FirebaseAnalytics(com.google.firebase.Firebase.analytics)
+
+actual class FirebaseAnalytics(val android: com.google.firebase.analytics.FirebaseAnalytics) {
+ actual fun logEvent(name: String, parameters: Map?) {
+ android.logEvent(name, parameters?.toBundle())
+ }
+ actual fun setUserProperty(name: String, value: String) {
+ android.setUserProperty(name, value)
+ }
+ actual fun setUserId(id: String) {
+ android.setUserId(id)
+ }
+ actual fun resetAnalyticsData() {
+ android.resetAnalyticsData()
+ }
+ actual fun setDefaultEventParameters(parameters: Map) {
+ android.setDefaultEventParameters(parameters.toBundle())
+ }
+
+ actual fun setAnalyticsCollectionEnabled(enabled: Boolean) {
+ android.setAnalyticsCollectionEnabled(enabled)
+ }
+
+ actual fun setSessionTimeoutInterval(sessionTimeoutInterval: Long) {
+ android.setSessionTimeoutDuration(sessionTimeoutInterval)
+ }
+
+ actual suspend fun getSessionId(): Long? = android.sessionId.await()
+
+ actual fun setConsent(consentSettings: Map) {
+ consentSettings.entries.associate {
+ it.key to when (it.value) {
+ ConsentStatus.GRANTED -> com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.GRANTED
+ ConsentStatus.DENIED -> com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.DENIED
+ }
+ }.let { androidConsentSettings ->
+ android.setConsent {
+ androidConsentSettings.entries.forEach {
+ when (it.key) {
+ ConsentType.AD_PERSONALIZATION ->
+ this.adPersonalization = it.value
+
+ ConsentType.AD_STORAGE ->
+ this.adStorage = it.value
+
+ ConsentType.AD_USER_DATA ->
+ this.adUserData = it.value
+
+ ConsentType.ANALYTICS_STORAGE ->
+ this.analyticsStorage = it.value
+ }
+ }
+
+ }
+ }
+ }
+
+ actual enum class ConsentType {
+ AD_PERSONALIZATION,
+ AD_STORAGE,
+ AD_USER_DATA,
+ ANALYTICS_STORAGE
+ }
+
+ actual enum class ConsentStatus {
+ GRANTED,
+ DENIED
+ }
+}
+
+actual class FirebaseAnalyticsException(message: String): Exception(message)
+
+private fun Map.toBundle() = Bundle().apply {
+ forEach { (key, value) ->
+ when(value::class) {
+ String::class -> putString(key, value as String)
+ Int::class -> putInt(key, value as Int)
+ Long::class -> putLong(key, value as Long)
+ Double::class -> putDouble(key, value as Double)
+ Boolean::class -> putBoolean(key, value as Boolean)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/firebase-analytics/src/androidUnitTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/androidUnitTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..aa0d883dd
--- /dev/null
+++ b/firebase-analytics/src/androidUnitTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,9 @@
+package dev.gitlive.firebase.analytics
+
+import org.junit.Ignore
+
+actual val emulatorHost: String = "10.0.2.2"
+
+actual val context: Any = ""
+
+actual typealias IgnoreForAndroidUnitTest = Ignore
diff --git a/firebase-analytics/src/commonMain/kotlin/dev/gitlive/firebase/analytics/AnalyticEventConstants.kt b/firebase-analytics/src/commonMain/kotlin/dev/gitlive/firebase/analytics/AnalyticEventConstants.kt
new file mode 100644
index 000000000..0484537d1
--- /dev/null
+++ b/firebase-analytics/src/commonMain/kotlin/dev/gitlive/firebase/analytics/AnalyticEventConstants.kt
@@ -0,0 +1,123 @@
+package dev.gitlive.firebase.analytics
+
+val FirebaseAnalytics.Event: FirebaseAnalyticsEvents
+ get() = FirebaseAnalyticsEvents
+
+object FirebaseAnalyticsEvents {
+ const val ADD_PAYMENT_INFO: String = "add_payment_info"
+ const val ADD_SHIPPING_INFO: String = "add_shipping_info"
+ const val ADD_TO_CART: String = "add_to_cart"
+ const val ADD_TO_WISHLIST: String = "add_to_wishlist"
+ const val AD_IMPRESSION: String = "ad_impression"
+ const val APP_OPEN: String = "app_open"
+ const val BEGIN_CHECKOUT: String = "begin_checkout"
+ const val CAMPAIGN_DETAILS: String = "campaign_details"
+ const val EARN_VIRTUAL_CURRENCY: String = "earn_virtual_currency"
+ const val GENERATE_LEAD: String = "generate_lead"
+ const val JOIN_GROUP: String = "join_group"
+ const val LEVEL_END: String = "level_end"
+ const val LEVEL_START: String = "level_start"
+ const val LEVEL_UP: String = "level_up"
+ const val LOGIN: String = "login"
+ const val POST_SCORE: String = "post_score"
+ const val PURCHASE: String = "purchase"
+ const val REFUND: String = "refund"
+ const val REMOVE_FROM_CART: String = "remove_from_cart"
+ const val SCREEN_VIEW: String = "screen_view"
+ const val SEARCH: String = "search"
+ const val SELECT_CONTENT: String = "select_content"
+ const val SELECT_ITEM: String = "select_item"
+ const val SELECT_PROMOTION: String = "select_promotion"
+ const val SHARE: String = "share"
+ const val SIGN_UP: String = "sign_up"
+ const val SPEND_VIRTUAL_CURRENCY: String = "spend_virtual_currency"
+ const val TUTORIAL_BEGIN: String = "tutorial_begin"
+ const val TUTORIAL_COMPLETE: String = "tutorial_complete"
+ const val UNLOCK_ACHIEVEMENT: String = "unlock_achievement"
+ const val VIEW_CART: String = "view_cart"
+ const val VIEW_ITEM: String = "view_item"
+ const val VIEW_ITEM_LIST: String = "view_item_list"
+ const val VIEW_PROMOTION: String = "view_promotion"
+ const val VIEW_SEARCH_RESULTS: String = "view_search_results"
+}
+
+val FirebaseAnalytics.Param: FirebaseAnalyticsParam
+ get() = FirebaseAnalyticsParam
+
+object FirebaseAnalyticsParam {
+ const val ACHIEVEMENT_ID: String = "achievement_id"
+ const val ACLID: String = "aclid"
+ const val AD_FORMAT: String = "ad_format"
+ const val AD_PLATFORM: String = "ad_platform"
+ const val AD_SOURCE: String = "ad_source"
+ const val AD_UNIT_NAME: String = "ad_unit_name"
+ const val AFFILIATION: String = "affiliation"
+ const val CAMPAIGN: String = "campaign"
+ const val CAMPAIGN_ID: String = "campaign_id"
+ const val CHARACTER: String = "character"
+ const val CONTENT: String = "content"
+ const val CONTENT_TYPE: String = "content_type"
+ const val COUPON: String = "coupon"
+ const val CP1: String = "cp1"
+ const val CREATIVE_FORMAT: String = "creative_format"
+ const val CREATIVE_NAME: String = "creative_name"
+ const val CREATIVE_SLOT: String = "creative_slot"
+ const val CURRENCY: String = "currency"
+ const val DESTINATION: String = "destination"
+ const val DISCOUNT: String = "discount"
+ const val END_DATE: String = "end_date"
+ const val EXTEND_SESSION: String = "extend_session"
+ const val FLIGHT_NUMBER: String = "flight_number"
+ const val GROUP_ID: String = "group_id"
+ const val INDEX: String = "index"
+ const val ITEMS: String = "items"
+ const val ITEM_BRAND: String = "item_brand"
+ const val ITEM_CATEGORY: String = "item_category"
+ const val ITEM_CATEGORY2: String = "item_category2"
+ const val ITEM_CATEGORY3: String = "item_category3"
+ const val ITEM_CATEGORY4: String = "item_category4"
+ const val ITEM_CATEGORY5: String = "item_category5"
+ const val ITEM_ID: String = "item_id"
+ const val ITEM_LIST_ID: String = "item_list_id"
+ const val ITEM_LIST_NAME: String = "item_list_name"
+ const val ITEM_NAME: String = "item_name"
+ const val ITEM_VARIANT: String = "item_variant"
+ const val LEVEL: String = "level"
+ const val LEVEL_NAME: String = "level_name"
+ const val LOCATION: String = "location"
+ const val LOCATION_ID: String = "location_id"
+ const val MARKETING_TACTIC: String = "marketing_tactic"
+ const val MEDIUM: String = "medium"
+ const val METHOD: String = "method"
+ const val NUMBER_OF_NIGHTS: String = "number_of_nights"
+ const val NUMBER_OF_PASSENGERS: String = "number_of_passengers"
+ const val NUMBER_OF_ROOMS: String = "number_of_rooms"
+ const val ORIGIN: String = "origin"
+ const val PAYMENT_TYPE: String = "payment_type"
+ const val PRICE: String = "price"
+ const val PROMOTION_ID: String = "promotion_id"
+ const val PROMOTION_NAME: String = "promotion_name"
+ const val QUANTITY: String = "quantity"
+ const val SCORE: String = "score"
+ const val SEARCH_TERM: String = "search_term"
+ const val SHIPPING: String = "shipping"
+ const val SHIPPING_TIER: String = "shipping_tier"
+ const val SOURCE: String = "source"
+ const val SOURCE_PLATFORM: String = "source_platform"
+ const val START_DATE: String = "start_date"
+ const val SUCCESS: String = "success"
+ const val TAX: String = "tax"
+ const val TERM: String = "term"
+ const val TRANSACTION_ID: String = "transaction_id"
+ const val TRAVEL_CLASS: String = "travel_class"
+ const val VALUE: String = "value"
+ const val VIRTUAL_CURRENCY_NAME: String = "virtual_currency_name"
+}
+
+val FirebaseAnalytics.UserProperty: FirebaseAnalyticsUserProperty
+ get() = FirebaseAnalyticsUserProperty
+
+object FirebaseAnalyticsUserProperty {
+ const val ALLOW_AD_PERSONALIZATION_SIGNALS: String = "allow_personalized_ads"
+ const val SIGN_UP_METHOD: String = "sign_up_method"
+}
diff --git a/firebase-analytics/src/commonMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/commonMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..a46f1688c
--- /dev/null
+++ b/firebase-analytics/src/commonMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,107 @@
+package dev.gitlive.firebase.analytics
+
+import dev.gitlive.firebase.Firebase
+import dev.gitlive.firebase.FirebaseApp
+
+expect val Firebase.analytics: FirebaseAnalytics
+
+/** Returns the [FirebaseStorage] instance of a given [FirebaseApp]. */
+expect fun Firebase.analytics(app: FirebaseApp): FirebaseAnalytics
+
+expect class FirebaseAnalytics {
+ fun logEvent(name: String, parameters: Map? = null)
+ fun setUserProperty(name: String, value: String)
+ fun setUserId(id: String)
+ fun setAnalyticsCollectionEnabled(enabled: Boolean)
+ fun setSessionTimeoutInterval(sessionTimeoutInterval: Long)
+ suspend fun getSessionId(): Long?
+ fun resetAnalyticsData()
+ fun setDefaultEventParameters(parameters: Map)
+ fun setConsent(consentSettings: Map)
+
+ enum class ConsentType {
+ AD_PERSONALIZATION,
+ AD_STORAGE,
+ AD_USER_DATA,
+ ANALYTICS_STORAGE
+ }
+
+ enum class ConsentStatus {
+ GRANTED,
+ DENIED
+ }
+}
+
+fun FirebaseAnalytics.setConsent(builder: FirebaseAnalyticsConsentBuilder.() -> Unit) {
+ val consentBuilder = FirebaseAnalyticsConsentBuilder()
+ consentBuilder.builder()
+ setConsent(consentBuilder.consentSettings)
+}
+
+fun FirebaseAnalytics.logEvent(name: String, builder: FirebaseAnalyticsParameters.() -> Unit) {
+ val params = FirebaseAnalyticsParameters()
+ params.builder()
+ logEvent(name, params.parameters)
+}
+
+expect class FirebaseAnalyticsException
+
+data class FirebaseAnalyticsParameters(
+ val parameters: MutableMap = mutableMapOf()
+) {
+ fun param(key: String, value: String) {
+ parameters[key] = value
+ }
+
+ fun param(key: String, value: Double) {
+ parameters[key] = value
+ }
+
+ fun param(key: String, value: Long) {
+ parameters[key] = value
+ }
+
+ fun param(key: String, value: Int) {
+ parameters[key] = value
+ }
+
+ fun param(key: String, value: Boolean) {
+ parameters[key] = value
+ }
+}
+
+data class FirebaseAnalyticsConsentBuilder(
+ val consentSettings: MutableMap = mutableMapOf()
+) {
+ var adPersonalization: FirebaseAnalytics.ConsentStatus?
+ get() = consentSettings[FirebaseAnalytics.ConsentType.AD_PERSONALIZATION]
+ set(value) {
+ value?.let {
+ consentSettings[FirebaseAnalytics.ConsentType.AD_PERSONALIZATION] = it
+ }
+ }
+
+ var adStorage: FirebaseAnalytics.ConsentStatus?
+ get() = consentSettings[FirebaseAnalytics.ConsentType.AD_STORAGE]
+ set(value) {
+ value?.let {
+ consentSettings[FirebaseAnalytics.ConsentType.AD_STORAGE] = it
+ }
+ }
+
+ var adUserData: FirebaseAnalytics.ConsentStatus?
+ get() = consentSettings[FirebaseAnalytics.ConsentType.AD_USER_DATA]
+ set(value) {
+ value?.let {
+ consentSettings[FirebaseAnalytics.ConsentType.AD_USER_DATA] = it
+ }
+ }
+
+ var analyticsStorage: FirebaseAnalytics.ConsentStatus?
+ get() = consentSettings[FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE]
+ set(value) {
+ value?.let {
+ consentSettings[FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE] = it
+ }
+ }
+}
\ No newline at end of file
diff --git a/firebase-analytics/src/commonTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/commonTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..0cd25e326
--- /dev/null
+++ b/firebase-analytics/src/commonTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.gitlive.firebase.analytics
+
+import dev.gitlive.firebase.Firebase
+import dev.gitlive.firebase.FirebaseOptions
+import dev.gitlive.firebase.apps
+import dev.gitlive.firebase.initialize
+import dev.gitlive.firebase.runBlockingTest
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+
+expect val emulatorHost: String
+expect val context: Any
+expect annotation class IgnoreForAndroidUnitTest()
+
+@IgnoreForAndroidUnitTest
+class FirebaseAnalyticsTest {
+
+ lateinit var analytics: FirebaseAnalytics
+
+ @BeforeTest
+ fun initializeFirebase() {
+ val app = Firebase.apps(context).firstOrNull() ?: Firebase.initialize(
+ context,
+ FirebaseOptions(
+ applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
+ apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
+ databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
+ storageBucket = "fir-kotlin-sdk.appspot.com",
+ projectId = "fir-kotlin-sdk",
+ gcmSenderId = "846484016111"
+ )
+ )
+
+ analytics = Firebase.analytics(app)
+ }
+
+ @AfterTest
+ fun deinitializeFirebase() = runBlockingTest {
+ Firebase.apps(context).forEach {
+ it.delete()
+ }
+ }
+
+ @Test
+ fun testAnalyticsShouldNotCrash() {
+ assertNotNull(analytics)
+
+ // This should not crash, otherwise the test will fail
+ analytics.logEvent("test") {
+ param("key", "value")
+ }
+ }
+}
\ No newline at end of file
diff --git a/firebase-analytics/src/iosMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/iosMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..aee47cbb6
--- /dev/null
+++ b/firebase-analytics/src/iosMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,89 @@
+package dev.gitlive.firebase.analytics
+
+import cocoapods.FirebaseAnalytics.FIRAnalytics
+import cocoapods.FirebaseAnalytics.setConsent
+import dev.gitlive.firebase.Firebase
+import dev.gitlive.firebase.FirebaseApp
+import dev.gitlive.firebase.FirebaseException
+import kotlinx.coroutines.CompletableDeferred
+import platform.Foundation.NSError
+import platform.Foundation.NSTimeInterval
+
+actual val Firebase.analytics: FirebaseAnalytics
+ get() = FirebaseAnalytics(FIRAnalytics)
+
+actual fun Firebase.analytics(app: FirebaseApp): FirebaseAnalytics = FirebaseAnalytics(FIRAnalytics)
+
+actual class FirebaseAnalytics(val ios: FIRAnalytics.Companion) {
+ actual fun logEvent(name: String, parameters: Map?) {
+ val mappedParameters: Map? = parameters?.map { it.key to it.value }?.toMap()
+ ios.logEventWithName(name, mappedParameters)
+ }
+ actual fun setUserProperty(name: String, value: String) {
+ ios.setUserPropertyString(value, name)
+ }
+ actual fun setUserId(id: String) {
+ ios.setUserID(id)
+ }
+ actual fun resetAnalyticsData() {
+ ios.resetAnalyticsData()
+ }
+
+ actual fun setAnalyticsCollectionEnabled(enabled: Boolean) {
+ ios.setAnalyticsCollectionEnabled(enabled)
+ }
+
+ actual fun setSessionTimeoutInterval(sessionTimeoutInterval: Long) {
+ ios.setSessionTimeoutInterval(sessionTimeoutInterval.toDouble())
+ }
+
+ actual suspend fun getSessionId(): Long? = ios.awaitResult { sessionIDWithCompletion(it) }
+
+ actual fun setDefaultEventParameters(parameters: Map) {
+ val mappedParameters: Map = parameters.map { it.key to it.value }.toMap()
+ ios.setDefaultEventParameters(mappedParameters)
+ }
+
+ actual fun setConsent(consentSettings: Map) {
+ val mappedConsentSettings: Map = consentSettings.map { it.key.name to it.value.name }.toMap()
+ ios.setConsent(mappedConsentSettings)
+ }
+
+ actual enum class ConsentType {
+ AD_PERSONALIZATION,
+ AD_STORAGE,
+ AD_USER_DATA,
+ ANALYTICS_STORAGE
+ }
+
+ actual enum class ConsentStatus {
+ GRANTED,
+ DENIED
+ }
+}
+
+actual class FirebaseAnalyticsException(message: String): FirebaseException(message)
+
+suspend inline fun T.await(function: T.(callback: (NSError?) -> Unit) -> Unit) {
+ val job = CompletableDeferred()
+ function { error ->
+ if(error == null) {
+ job.complete(Unit)
+ } else {
+ job.completeExceptionally(FirebaseAnalyticsException(error.toString()))
+ }
+ }
+ job.await()
+}
+
+suspend inline fun T.awaitResult(function: T.(callback: (R?, NSError?) -> Unit) -> Unit): R {
+ val job = CompletableDeferred()
+ function { result, error ->
+ if(error == null) {
+ job.complete(result)
+ } else {
+ job.completeExceptionally(FirebaseAnalyticsException(error.toString()))
+ }
+ }
+ return job.await() as R
+}
\ No newline at end of file
diff --git a/firebase-analytics/src/iosTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/iosTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..722724e53
--- /dev/null
+++ b/firebase-analytics/src/iosTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.gitlive.firebase.analytics
+
+actual val emulatorHost: String = "127.0.0.1"
+
+actual val context: Any = Unit
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+actual annotation class IgnoreForAndroidUnitTest
diff --git a/firebase-analytics/src/jsMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/jsMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..413203706
--- /dev/null
+++ b/firebase-analytics/src/jsMain/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,97 @@
+package dev.gitlive.firebase.analytics
+
+import dev.gitlive.firebase.Firebase
+import dev.gitlive.firebase.FirebaseApp
+import dev.gitlive.firebase.FirebaseException
+import dev.gitlive.firebase.analytics.externals.getAnalytics
+import kotlinx.coroutines.await
+
+actual val Firebase.analytics: FirebaseAnalytics
+ get() = FirebaseAnalytics(getAnalytics())
+
+actual fun Firebase.analytics(app: FirebaseApp) =
+ FirebaseAnalytics(getAnalytics(app.js))
+
+actual class FirebaseAnalytics(val js: dev.gitlive.firebase.analytics.externals.FirebaseAnalytics) {
+ actual fun logEvent(
+ name: String,
+ parameters: Map?
+ ) {
+ dev.gitlive.firebase.analytics.externals.logEvent(js, name, parameters)
+ }
+
+ actual fun setUserProperty(name: String, value: String) {
+ dev.gitlive.firebase.analytics.externals.setUserProperty(js, name, value)
+ }
+
+ actual fun setUserId(id: String) {
+ dev.gitlive.firebase.analytics.externals.setUserId(js, id)
+ }
+
+ actual fun setAnalyticsCollectionEnabled(enabled: Boolean) {
+ dev.gitlive.firebase.analytics.externals.setAnalyticsCollectionEnabled(js, enabled)
+ }
+
+ actual fun setSessionTimeoutInterval(sessionTimeoutInterval: Long) {
+ dev.gitlive.firebase.analytics.externals.setSessionTimeoutInterval(js, sessionTimeoutInterval)
+ }
+
+ actual suspend fun getSessionId(): Long? = rethrow { dev.gitlive.firebase.analytics.externals.getSessionId(js).await() }
+
+ actual fun resetAnalyticsData() {
+ dev.gitlive.firebase.analytics.externals.resetAnalyticsData(js)
+ }
+
+ actual fun setDefaultEventParameters(parameters: Map) {
+ dev.gitlive.firebase.analytics.externals.setDefaultEventParameters(js, parameters)
+ }
+
+ actual fun setConsent(consentSettings: Map) {
+ val consent = dev.gitlive.firebase.analytics.externals.ConsentSettings()
+ consentSettings.forEach {
+ when (it.key) {
+ ConsentType.AD_PERSONALIZATION -> consent.ad_personalization = it.value.name
+ ConsentType.AD_STORAGE -> consent.ad_storage = it.value.name
+ ConsentType.AD_USER_DATA -> consent.ad_user_data = it.value.name
+ ConsentType.ANALYTICS_STORAGE -> consent.analytics_storage = it.value.name
+ }
+ }
+ dev.gitlive.firebase.analytics.externals.setConsent(js, consent)
+ }
+
+ actual enum class ConsentType {
+ AD_PERSONALIZATION,
+ AD_STORAGE,
+ AD_USER_DATA,
+ ANALYTICS_STORAGE
+ }
+
+ actual enum class ConsentStatus {
+ GRANTED,
+ DENIED
+ }
+}
+
+actual open class FirebaseAnalyticsException(code: String, cause: Throwable): FirebaseException(code, cause)
+
+internal inline fun rethrow(function: () -> R): R {
+ try {
+ return function()
+ } catch (e: Exception) {
+ throw e
+ } catch (e: dynamic) {
+ throw errorToException(e)
+ }
+}
+
+internal fun errorToException(error: dynamic) = (error?.code ?: error?.message ?: "")
+ .toString()
+ .lowercase()
+ .let { code ->
+ when {
+ else -> {
+ println("Unknown error code in ${JSON.stringify(error)}")
+ FirebaseAnalyticsException(code, error)
+ }
+ }
+ }
\ No newline at end of file
diff --git a/firebase-analytics/src/jsMain/kotlin/dev/gitlive/firebase/analytics/externals/analytics.kt b/firebase-analytics/src/jsMain/kotlin/dev/gitlive/firebase/analytics/externals/analytics.kt
new file mode 100644
index 000000000..ceded59d4
--- /dev/null
+++ b/firebase-analytics/src/jsMain/kotlin/dev/gitlive/firebase/analytics/externals/analytics.kt
@@ -0,0 +1,32 @@
+@file:JsModule("firebase/analytics")
+@file:JsNonModule
+
+package dev.gitlive.firebase.analytics.externals
+
+import dev.gitlive.firebase.externals.FirebaseApp
+import kotlin.js.Promise
+
+
+external fun getAnalytics(app: FirebaseApp? = definedExternally): FirebaseAnalytics
+
+external fun logEvent(app: FirebaseAnalytics, name: String, parameters: Map?)
+external fun setUserProperty(app: FirebaseAnalytics, name: String, value: String)
+external fun setUserId(app: FirebaseAnalytics, id: String)
+external fun resetAnalyticsData(app: FirebaseAnalytics)
+external fun setDefaultEventParameters(app: FirebaseAnalytics, parameters: Map)
+external fun setAnalyticsCollectionEnabled(app: FirebaseAnalytics, enabled: Boolean)
+external fun setSessionTimeoutInterval(app: FirebaseAnalytics, sessionTimeoutInterval: Long)
+external fun getSessionId(app: FirebaseAnalytics): Promise
+external fun setConsent(app: FirebaseAnalytics, consentSettings: ConsentSettings)
+
+external interface FirebaseAnalytics
+
+external class ConsentSettings() {
+ var ad_personalization: String?
+ var ad_storage: String?
+ var ad_user_data: String?
+ var analytics_storage: String?
+ var functionality_storage: String?
+ var personalization_storage: String?
+ var security_storage: String?
+}
diff --git a/firebase-analytics/src/jsTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/jsTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..117266228
--- /dev/null
+++ b/firebase-analytics/src/jsTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,8 @@
+package dev.gitlive.firebase.analytics
+
+actual val emulatorHost: String = "10.0.2.2"
+
+actual val context: Any = Unit
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+actual annotation class IgnoreForAndroidUnitTest
\ No newline at end of file
diff --git a/firebase-analytics/src/jvmMain/kotlin/dev/gitlive/firebase/analytics/analytics.jvm.kt b/firebase-analytics/src/jvmMain/kotlin/dev/gitlive/firebase/analytics/analytics.jvm.kt
new file mode 100644
index 000000000..50fb3da39
--- /dev/null
+++ b/firebase-analytics/src/jvmMain/kotlin/dev/gitlive/firebase/analytics/analytics.jvm.kt
@@ -0,0 +1,35 @@
+package dev.gitlive.firebase.analytics
+
+import dev.gitlive.firebase.Firebase
+import dev.gitlive.firebase.FirebaseApp
+import dev.gitlive.firebase.FirebaseException
+
+actual val Firebase.analytics: FirebaseAnalytics
+ get() = TODO("Not yet implemented")
+
+actual fun Firebase.analytics(app: FirebaseApp): FirebaseAnalytics {
+ TODO("Not yetimplemented")
+}
+
+actual class FirebaseAnalytics {
+ actual fun setUserProperty(name: String, value: String) {}
+ actual fun setUserId(id: String) {}
+ actual fun resetAnalyticsData() {}
+ actual fun setAnalyticsCollectionEnabled(enabled: Boolean) {}
+ actual fun setSessionTimeoutInterval(sessionTimeoutInterval: Long) {}
+ actual suspend fun getSessionId(): Long? = TODO("Not yet implemented")
+ actual fun setDefaultEventParameters(parameters: Map) {}
+ actual fun logEvent(name: String, parameters: Map?) {}
+
+ actual fun setConsent(consentSettings: Map) {}
+
+ actual enum class ConsentType {
+ AD_PERSONALIZATION, AD_STORAGE, AD_USER_DATA, ANALYTICS_STORAGE
+ }
+
+ actual enum class ConsentStatus {
+ GRANTED, DENIED
+ }
+}
+
+actual class FirebaseAnalyticsException internal constructor(message: String) : FirebaseException(message)
\ No newline at end of file
diff --git a/firebase-analytics/src/jvmTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt b/firebase-analytics/src/jvmTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
new file mode 100644
index 000000000..d907efc70
--- /dev/null
+++ b/firebase-analytics/src/jvmTest/kotlin/dev/gitlive/firebase/analytics/analytics.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+@file:JvmName("tests")
+package dev.gitlive.firebase.analytics
+
+actual val emulatorHost: String = "10.0.2.2"
+
+actual val context: Any = Unit
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+actual annotation class IgnoreForAndroidUnitTest
diff --git a/gradle.properties b/gradle.properties
index fcc583fd6..3fc365770 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,6 +21,8 @@ kotlin.native.cacheKind=none
# Set to true to skip tests and even compilation of the iOS target.
skipIosTarget=false
# Skip iOS Tests
+# We are skipping analytics ios tests due to an issue with cocoapods not working as expected with binary distributed dependencies
+firebase-analytics.skipIosTests=true
firebase-app.skipIosTests=false
# We are skipping auth ios tests due to an issue with keychain and simulator.
firebase-auth.skipIosTests=true
@@ -37,6 +39,7 @@ firebase-crashlytics.skipIosTests=false
firebase-storage.skipIosTests=false
# We can have the functionality to skip js tests, due to compatibility issues.
+firebase-analytics.skipJsTests=false
firebase-app.skipJsTests=false
firebase-auth.skipJsTests=false
firebase-common.skipJsTests=false
@@ -51,6 +54,7 @@ firebase-perf.skipJsTests=false
firebase-storage.skipJsTests=false
# Versions:
+firebase-analytics.version=1.12.0
firebase-app.version=1.12.0
firebase-auth.version=1.12.0
firebase-common.version=1.12.0
diff --git a/settings.gradle.kts b/settings.gradle.kts
index dff3511a3..c0f89e23b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,4 +1,5 @@
include(
+ "firebase-analytics",
"firebase-app",
"firebase-auth",
"firebase-common",