chore(android): integrate firebase-crashlytics (#11808)

This commit is contained in:
Aki Chang
2025-04-21 10:37:31 +08:00
committed by GitHub
parent f82f213373
commit e3973538e8
20 changed files with 334 additions and 158 deletions

View File

@@ -1,2 +1,3 @@
/build/*
!/build/.npmkeep
google-services.json

View File

@@ -3,13 +3,15 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
alias libs.plugins.android.application
alias libs.plugins.compose
alias libs.plugins.hilt
alias libs.plugins.kotlin.android
alias libs.plugins.kotlin.parcelize
alias libs.plugins.kotlin.serialization
alias libs.plugins.ksp
alias libs.plugins.rust.android
alias libs.plugins.compose
alias libs.plugins.google.service
alias libs.plugins.firebase.crashlytics
alias libs.plugins.hilt
}
apply from: 'capacitor.build.gradle'
@@ -106,6 +108,7 @@ dependencies {
implementation libs.androidx.coordinatorlayout
implementation libs.androidx.core.ktx
implementation libs.androidx.core.splashscreen
implementation libs.androidx.datastore.preferences
implementation libs.androidx.navigation.fragment
implementation libs.androidx.navigation.ui.ktx
@@ -118,8 +121,11 @@ dependencies {
implementation libs.kotlinx.coroutines.android
implementation libs.kotlinx.serialization.json
def okhttpBom = platform(libs.okhttp.bom)
implementation okhttpBom
implementation platform(libs.firebase.bom)
implementation libs.firebase.analytics
implementation libs.firebase.crashlytics
implementation platform(libs.okhttp.bom)
implementation libs.okhttp
implementation libs.okhttp.coroutines
implementation libs.okhttp.logging
@@ -130,15 +136,6 @@ dependencies {
androidTestImplementation libs.androidx.espresso.core
}
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch (Exception ignored) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
cargo {
module = "../../../../mobile-native"
libname = "affine_mobile_native"

View File

@@ -68,3 +68,8 @@
-keepclassmembers public class **$$serializer {
private ** descriptor;
}
# Keep file names and line numbers.
-keepattributes SourceFile,LineNumberTable
# Keep custom exceptions.
-keep public class * extends java.lang.Exception

View File

@@ -1,7 +1,22 @@
package app.affine.pro
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import app.affine.pro.service.CookieStore
import app.affine.pro.utils.dataStore
import app.affine.pro.utils.get
import app.affine.pro.utils.logger.AffineDebugTree
import app.affine.pro.utils.logger.CrashlyticsTree
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.ktx.Firebase
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
@HiltAndroidApp
@@ -9,8 +24,47 @@ class AffineApp : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
_context = applicationContext
// init logger
Timber.plant(if (BuildConfig.DEBUG) AffineDebugTree() else CrashlyticsTree())
// init capacitor config
CapacitorConfig.init(baseContext)
// init crashlytics
Firebase.crashlytics.setCustomKeys {
key("affine_version", CapacitorConfig.getAffineVersion())
}
// init cookies from local
MainScope().launch(Dispatchers.IO) {
val sessionCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_SESSION)
val userIdCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_USER_ID)
if (sessionCookieStr.isEmpty() || userIdCookieStr.isEmpty()) {
Timber.i("[init] user has not signed in yet.")
return@launch
}
Timber.i("[init] user already signed in.")
try {
val cookies = listOf(
Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), sessionCookieStr)
?: error("Parse session cookie fail:[ cookie = $sessionCookieStr ]"),
Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), userIdCookieStr)
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
)
CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies)
} catch (e: Exception) {
Timber.w(e, "[init] load persistent cookies fail.")
}
}
}
override fun onTerminate() {
_context = null
super.onTerminate()
}
companion object {
@SuppressLint("StaticFieldLeak")
private var _context: Context? = null
fun context() = requireNotNull(_context)
}
}

View File

@@ -1,4 +1,4 @@
package app.affine.pro.ai
package app.affine.pro
enum class Prompt(val value: String) {
Summary("Summary"),

View File

@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.coroutineContext
@HiltViewModel
class ChatViewModel @Inject constructor(
@@ -37,10 +36,10 @@ class ChatViewModel @Inject constructor(
workspaceId = webRepo.workspaceId(),
docId = webRepo.docId(),
).getOrElse {
Timber.d("Create session failed")
Timber.w(it, "Create session failed")
return@launch
}
Timber.d("Create session: $sessionId")
Timber.i("Create session success:[ sessionId = $sessionId].")
val historyMessages = graphQLRepo.getCopilotHistories(
workspaceId = webRepo.workspaceId(),
docId = webRepo.docId(),
@@ -60,16 +59,12 @@ class ChatViewModel @Inject constructor(
sessionId = sessionId,
message = message,
).onSuccess { messageId ->
Timber.d("send message: $messageId")
Timber.i("send message: $messageId")
sseRepo.messageStream(sessionId, messageId)
.onEach {
Timber.d("$coroutineContext")
Timber.d("on message: ${it.getOrNull()}")
Timber.d("On sse message: ${it.getOrNull()}")
}
.flowOn(Dispatchers.IO)
.onEach {
Timber.d("$coroutineContext")
}
.collect()
}
}
@@ -80,7 +75,7 @@ class ChatViewModel @Inject constructor(
docId = webRepo.docId(),
).onSuccess { id ->
sessionId = id
Timber.d("Create session: $id")
Timber.i("Create session: $id")
sendMessage()
}.onFailure {
Timber.e(it, "Create session failed.")

View File

@@ -4,6 +4,7 @@ import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import timber.log.Timber
@CapacitorPlugin(name = "AIButton")
class AIButtonPlugin : Plugin() {
@@ -16,6 +17,7 @@ class AIButtonPlugin : Plugin() {
@PluginMethod
fun present(call: PluginCall) {
launch {
Timber.i("present AIButton")
(activity as? Callback)?.present()
call.resolve()
}
@@ -24,6 +26,7 @@ class AIButtonPlugin : Plugin() {
@PluginMethod
fun dismiss(call: PluginCall) {
launch {
Timber.i("dismiss AIButton")
(activity as? Callback)?.dismiss()
call.resolve()
}

View File

@@ -4,6 +4,7 @@ import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import timber.log.Timber
@CapacitorPlugin(name = "AffineTheme")
class AffineThemePlugin : Plugin() {
@@ -14,7 +15,9 @@ class AffineThemePlugin : Plugin() {
@PluginMethod
fun onThemeChanged(call: PluginCall) {
(bridge.activity as? Callback)?.onThemeChanged(call.data.optBoolean("darkMode"))
val darkMode = call.data.optBoolean("darkMode")
Timber.i("onThemeChanged:[ darkMode = $darkMode ]")
(bridge.activity as? Callback)?.onThemeChanged(darkMode)
call.resolve()
}
}

View File

@@ -1,9 +1,12 @@
package app.affine.pro.plugin
import android.annotation.SuppressLint
import app.affine.pro.AffineApp
import app.affine.pro.CapacitorConfig
import app.affine.pro.service.CookieStore
import app.affine.pro.service.OkHttp
import app.affine.pro.utils.clear
import app.affine.pro.utils.dataStore
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
@@ -16,6 +19,7 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.coroutines.executeAsync
import org.json.JSONObject
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
@CapacitorPlugin(name = "Auth")
@@ -23,120 +27,18 @@ class AuthPlugin : Plugin() {
@PluginMethod
fun signInMagicLink(call: PluginCall) {
launch(Dispatchers.IO) {
try {
val endpoint = call.getStringEnsure("endpoint")
val email = call.getStringEnsure("email")
val token = call.getStringEnsure("token")
val clientNonce = call.getString("clientNonce")
val body = JSONObject()
.apply {
put("email", email)
put("token", token)
put("client_nonce", clientNonce)
}
.toString()
.toRequestBody("application/json".toMediaTypeOrNull())
val request = Request.Builder()
.url("$endpoint/api/auth/magic-link")
.header("x-affine-version", CapacitorConfig.getAffineVersion())
.post(body)
.build()
OkHttp.client.newCall(request).executeAsync().use { response ->
if (response.code >= 400) {
call.reject(response.body.string())
return@launch
}
CookieStore.getCookie(endpoint, "affine_session")?.let {
call.resolve(JSObject().put("token", it))
} ?: call.reject("token not found")
}
} catch (e: Exception) {
call.reject("Failed to sign in, $e", null, e)
}
}
processSignIn(call, SignInMethod.MagicLink)
}
@PluginMethod
fun signInOauth(call: PluginCall) {
launch(Dispatchers.IO) {
try {
val endpoint = call.getStringEnsure("endpoint")
val code = call.getStringEnsure("code")
val state = call.getStringEnsure("state")
val clientNonce = call.getString("clientNonce")
val body = JSONObject()
.apply {
put("code", code)
put("state", state)
put("client_nonce", clientNonce)
}
.toString()
.toRequestBody("application/json".toMediaTypeOrNull())
val request = Request.Builder()
.url("$endpoint/api/oauth/callback")
.header("x-affine-version", CapacitorConfig.getAffineVersion())
.post(body)
.build()
OkHttp.client.newCall(request).executeAsync().use { response ->
if (response.code >= 400) {
call.reject(response.body.string())
return@launch
}
CookieStore.getCookie(endpoint, "affine_session")?.let {
call.resolve(JSObject().put("token", it))
} ?: call.reject("token not found")
}
} catch (e: Exception) {
call.reject("Failed to sign in, $e", null, e)
}
}
processSignIn(call, SignInMethod.Oauth)
}
@SuppressLint("BuildListAdds")
@PluginMethod
fun signInPassword(call: PluginCall) {
launch(Dispatchers.IO) {
try {
val endpoint = call.getStringEnsure("endpoint")
val email = call.getStringEnsure("email")
val password = call.getStringEnsure("password")
val verifyToken = call.getString("verifyToken")
val challenge = call.getString("challenge")
val body = JSONObject()
.apply {
put("email", email)
put("password", password)
}
.toString()
.toRequestBody("application/json".toMediaTypeOrNull())
val requestBuilder = Request.Builder()
.url("$endpoint/api/auth/sign-in")
.header("x-affine-version", CapacitorConfig.getAffineVersion())
.post(body)
if (verifyToken != null) {
requestBuilder.addHeader("x-captcha-token", verifyToken)
}
if (challenge != null) {
requestBuilder.addHeader("x-captcha-challenge", challenge)
}
OkHttp.client.newCall(requestBuilder.build()).executeAsync().use { response ->
if (response.code >= 400) {
call.reject(response.body.string())
return@launch
}
CookieStore.getCookie(endpoint, "affine_session")?.let {
call.resolve(JSObject().put("token", it))
} ?: call.reject("token not found")
}
} catch (e: Exception) {
call.reject("Failed to sign in, $e", null, e)
}
}
processSignIn(call, SignInMethod.Password)
}
@PluginMethod
@@ -154,11 +56,110 @@ class AuthPlugin : Plugin() {
call.reject(response.body.string())
return@launch
}
Timber.i("Sign out success.")
call.resolve(JSObject().put("ok", true))
}
} catch (e: Exception) {
Timber.w(e, "Sign out fail.")
call.reject("Failed to sign out, $e", null, e)
}
}
}
private enum class SignInMethod {
Password, Oauth, MagicLink
}
private fun processSignIn(call: PluginCall, method: SignInMethod) {
launch(Dispatchers.IO) {
try {
val endpoint = call.getStringEnsure("endpoint")
val request = when (method) {
SignInMethod.Password -> {
val email = call.getStringEnsure("email")
val password = call.getStringEnsure("password")
val verifyToken = call.getString("verifyToken")
val challenge = call.getString("challenge")
val body = JSONObject()
.apply {
put("email", email)
put("password", password)
}
.toString()
.toRequestBody("application/json".toMediaTypeOrNull())
val requestBuilder = Request.Builder()
.url("$endpoint/api/auth/sign-in")
.header("x-affine-version", CapacitorConfig.getAffineVersion())
.post(body)
if (verifyToken != null) {
requestBuilder.addHeader("x-captcha-token", verifyToken)
}
if (challenge != null) {
requestBuilder.addHeader("x-captcha-challenge", challenge)
}
requestBuilder.build()
}
SignInMethod.Oauth -> {
val code = call.getStringEnsure("code")
val state = call.getStringEnsure("state")
val clientNonce = call.getString("clientNonce")
val body = JSONObject()
.apply {
put("code", code)
put("state", state)
put("client_nonce", clientNonce)
}
.toString()
.toRequestBody("application/json".toMediaTypeOrNull())
Request.Builder()
.url("$endpoint/api/oauth/callback")
.header("x-affine-version", CapacitorConfig.getAffineVersion())
.post(body)
.build()
}
SignInMethod.MagicLink -> {
val email = call.getStringEnsure("email")
val token = call.getStringEnsure("token")
val clientNonce = call.getString("clientNonce")
val body = JSONObject()
.apply {
put("email", email)
put("token", token)
put("client_nonce", clientNonce)
}
.toString()
.toRequestBody("application/json".toMediaTypeOrNull())
Request.Builder()
.url("$endpoint/api/auth/magic-link")
.header("x-affine-version", CapacitorConfig.getAffineVersion())
.post(body)
.build()
}
}
OkHttp.client.newCall(request).executeAsync().use { response ->
if (response.code >= 400) {
call.reject(response.body.string())
return@launch
}
CookieStore.getCookie(endpoint, CookieStore.AFFINE_SESSION)?.let {
Timber.i("$method sign in success.")
Timber.d("Update session [$it]")
call.resolve(JSObject().put("token", it))
} ?: run {
Timber.w("$method sign in fail, token not found.")
call.reject("$method sign in fail, token not found")
}
}
} catch (e: Exception) {
Timber.w(e, "$method sign in fail.")
call.reject("$method sign in fail.", null, e)
}
}
}
}

View File

@@ -6,6 +6,7 @@ import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
import timber.log.Timber
import uniffi.affine_mobile_native.hashcashMint
@CapacitorPlugin(name = "HashCash")
@@ -16,8 +17,10 @@ class HashCashPlugin : Plugin() {
launch(Dispatchers.IO) {
val challenge = call.getString("challenge") ?: ""
val bits = call.getInt("bits") ?: 20
val hash = hashcashMint(resource = challenge, bits = bits.toUInt())
Timber.i("hash:[ value = $hash ]")
call.resolve(JSObject().apply {
put("value", hashcashMint(resource = challenge, bits = bits.toUInt()))
put("value", hash)
})
}
}

View File

@@ -28,7 +28,8 @@ class NbStorePlugin : Plugin() {
val spaceType = call.getStringEnsure("spaceType")
val peer = call.getStringEnsure("peer")
val appStoragePath = activity?.filesDir ?: run {
call.reject("Failed to connect storage, cannot access file system.")
Timber.w("Failed to connect storage, cannot access device file system.")
call.reject("Failed to connect storage, cannot access device file system.")
return@launch
}
val peerDir = appStoragePath.resolve("workspaces")
@@ -38,13 +39,15 @@ class NbStorePlugin : Plugin() {
.replace(Regex("_+"), "_")
.replace(Regex("_+$"), "")
)
Timber.d("connecting nbstore... peerDir[$peerDir]")
Timber.i("NbStore connecting... peerDir[$peerDir].")
peerDir.mkdirs()
val db = peerDir.resolve("$spaceId.db")
docStoragePool.connect(id, db.path)
Timber.i("NbStore connected [ id = $id ].")
call.resolve()
} catch (e: Exception) {
call.reject("Failed to connect storage", e)
Timber.e(e, "Failed to connect NbStore.")
call.reject("Failed to connect NbStore.", e)
}
}
}
@@ -55,9 +58,11 @@ class NbStorePlugin : Plugin() {
try {
val id = call.getStringEnsure("id")
docStoragePool.disconnect(universalId = id)
Timber.i("NbStore disconnected [ id = $id ].")
call.resolve()
} catch (e: Exception) {
call.reject("Failed to disconnect, ${e.message}", null, e)
Timber.e(e, "Failed to disconnect NbStore")
call.reject("Failed to disconnect NbStore", null, e)
}
}
}
@@ -69,8 +74,10 @@ class NbStorePlugin : Plugin() {
val id = call.getStringEnsure("id")
val spaceId = call.getStringEnsure("spaceId")
docStoragePool.setSpaceId(universalId = id, spaceId = spaceId)
Timber.i("Set space id: [ id = $id, spaceId = $spaceId ].")
call.resolve()
} catch (e: Exception) {
Timber.e(e, "Failed to set space id.")
call.reject("Failed to set space id, ${e.message}", null, e)
}
}

View File

@@ -1,6 +1,6 @@
package app.affine.pro.repo
import app.affine.pro.ai.Prompt
import app.affine.pro.Prompt
import app.affine.pro.service.GraphQLClient
import com.affine.pro.graphql.CreateCopilotMessageMutation
import com.affine.pro.graphql.CreateCopilotSessionMutation

View File

@@ -1,6 +1,14 @@
package app.affine.pro.service
import androidx.core.net.toUri
import app.affine.pro.AffineApp
import app.affine.pro.utils.dataStore
import app.affine.pro.utils.set
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -26,7 +34,6 @@ object OkHttp {
}
})
.addInterceptor(HttpLoggingInterceptor { msg ->
Timber.tag("Affine-Network")
Timber.d(msg)
}.apply {
level = HttpLoggingInterceptor.Level.BODY
@@ -37,10 +44,23 @@ object OkHttp {
object CookieStore {
const val AFFINE_SESSION = "affine_session"
const val AFFINE_USER_ID = "affine_user_id"
private val _cookies = ConcurrentHashMap<String, List<Cookie>>()
fun saveCookies(host: String, cookies: List<Cookie>) {
_cookies[host] = cookies
MainScope().launch(Dispatchers.IO) {
cookies.find { it.name == AFFINE_SESSION }?.let {
AffineApp.context().dataStore.set(AFFINE_SESSION, it.toString())
}
cookies.find { it.name == AFFINE_USER_ID }?.let {
Timber.d("Update user id [${it.value}]")
AffineApp.context().dataStore.set(AFFINE_USER_ID, it.toString())
Firebase.crashlytics.setUserId(it.value)
}
}
}
fun getCookies(host: String) = _cookies[host] ?: emptyList()
@@ -49,5 +69,4 @@ object CookieStore {
?.let { _cookies[it] }
?.find { cookie -> cookie.name == name }
?.value
}

View File

@@ -0,0 +1,30 @@
package app.affine.pro.utils
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "affine")
suspend fun DataStore<Preferences>.set(key: String, value: String) {
edit {
it[stringPreferencesKey(key)] = value
}
}
suspend fun DataStore<Preferences>.get(key: String) = data.map {
it[stringPreferencesKey(key)] ?: ""
}.first()
suspend fun DataStore<Preferences>.clear(vararg keys: String) {
edit { prefs ->
keys.forEach { key ->
prefs[stringPreferencesKey(key)] = ""
}
}
}

View File

@@ -0,0 +1,10 @@
package app.affine.pro.utils.logger
import timber.log.Timber
class AffineDebugTree : Timber.DebugTree() {
override fun createStackElementTag(element: StackTraceElement): String {
return "Affine:${super.createStackElementTag(element)}:${element.lineNumber}"
}
}

View File

@@ -0,0 +1,36 @@
package app.affine.pro.utils.logger
import android.util.Log
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import timber.log.Timber
class CrashlyticsTree : Timber.Tree() {
private val crashlytics = Firebase.crashlytics
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority < Log.INFO) {
return
}
val level = when (priority) {
Log.ASSERT -> "[assert]"
Log.ERROR -> "[error]"
Log.WARN -> "[warn]"
else -> "[info]"
}
crashlytics.log(
StringBuilder(level)
.append(tag?.let { "[$tag]" } ?: "")
.append(" ")
.append(message)
.toString()
)
if (t == null) return
crashlytics.recordException(t)
}
}

View File

@@ -21,15 +21,17 @@ plugins {
alias libs.plugins.version.catalog.update
alias libs.plugins.android.application apply false
alias libs.plugins.android.library apply false
alias libs.plugins.apollo.android apply false
alias libs.plugins.compose apply false
alias libs.plugins.firebase.crashlytics apply false
alias libs.plugins.google.service apply false
alias libs.plugins.hilt apply false
alias libs.plugins.jetbrains.kotlin.jvm apply false
alias libs.plugins.kotlin.android apply false
alias libs.plugins.kotlin.parcelize apply false
alias libs.plugins.kotlin.serialization apply false
alias libs.plugins.compose apply false
alias libs.plugins.ksp apply false
alias libs.plugins.hilt apply false
alias libs.plugins.rust.android apply false
alias libs.plugins.apollo.android apply false
alias libs.plugins.jetbrains.kotlin.jvm apply false
}
apply from: "variables.gradle"

View File

@@ -1,13 +1,11 @@
versionCatalogUpdate {
sortByKey.set(true)
keep {
// keep versions without any library or plugin reference
keepUnusedVersions.set(true)
// keep all libraries that aren't used in the project
keepUnusedLibraries.set(true)
// keep all plugins that aren't used in the project
keepUnusedPlugins.set(true)
sortByKey = true
versionCatalogs {
special {
keep {
keepUnusedVersions = true
}
}
}
}

View File

@@ -1,5 +1,4 @@
[versions]
# @keep
android-gradle-plugin = "8.9.1"
androidx-activity-compose = "1.10.1"
androidx-appcompat = "1.7.0"
@@ -8,6 +7,7 @@ androidx-compose-bom = "2025.04.00"
androidx-coordinatorlayout = "1.3.0"
androidx-core-ktx = "1.16.0"
androidx-core-splashscreen = "1.0.1"
androidx-datastore-preferences = "1.1.4"
androidx-espresso-core = "3.6.1"
androidx-junit = "1.2.1"
androidx-lifecycle-compose = "2.8.7"
@@ -17,6 +17,8 @@ apollo = "4.1.1"
apollo-kotlin-adapters = "0.0.4"
# @keep
compileSdk = "35"
firebase-bom = "33.12.0"
firebase-crashlytics = "3.0.3"
google-services = "4.4.2"
gradle-versions = "0.52.0"
hilt = "2.56.1"
@@ -31,11 +33,11 @@ ksp = "2.1.20-2.0.0"
# @keep
minSdk = "22"
mozilla-rust-android = "0.9.6"
okhttp = "5.0.0-alpha.14"
okhttp-bom = "5.0.0-alpha.14"
# @keep
targetSdk = "35"
timber = "5.0.1"
version-catalog-update = "0.8.5"
version-catalog-update = "1.0.0"
[libraries]
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
@@ -56,6 +58,7 @@ androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding
androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "androidx-coordinatorlayout" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore-preferences" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso-core" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-compose" }
@@ -63,10 +66,13 @@ androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-v
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
apollo-adapters-core = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-kotlin-adapters"}
apollo-adapters-kotlinx-datetime = { module = "com.apollographql.adapters:apollo-adapters-kotlinx-datetime", version.ref = "apollo-kotlin-adapters"}
apollo-adapters-core = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-kotlin-adapters" }
apollo-adapters-kotlinx-datetime = { module = "com.apollographql.adapters:apollo-adapters-kotlinx-datetime", version.ref = "apollo-kotlin-adapters" }
apollo-api = { module = "com.apollographql.apollo:apollo-api", version.ref = "apollo" }
apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
@@ -79,8 +85,8 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
okhttp = { module = "com.squareup.okhttp3:okhttp" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp-bom" }
okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp-sse = { module = "com.squareup.okhttp3:okhttp-sse" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
@@ -90,6 +96,8 @@ android-application = { id = "com.android.application", version.ref = "android-g
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
apollo-android = { id = "com.apollographql.apollo", version.ref = "apollo" }
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" }
google-service = { id = "com.google.gms.google-services", version.ref = "google-services" }
gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }