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",