feat(analytics): Firebase Analytics, Crashlytics, ObservabilityModule, Firestore rules
This commit is contained in:
parent
11a81cb826
commit
1a33d4f2b9
|
|
@ -73,6 +73,9 @@ dependencies {
|
|||
implementation("com.google.firebase:firebase-config-ktx")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||
implementation("com.google.firebase:firebase-appcheck-ktx")
|
||||
implementation("com.google.firebase:firebase-appcheck-playintegrity")
|
||||
debugImplementation("com.google.firebase:firebase-appcheck-debug")
|
||||
|
||||
// Hilt
|
||||
implementation("com.google.dagger:hilt-android:2.53.1")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
package com.couplesconnect.app
|
||||
|
||||
import android.app.Application
|
||||
import com.couplesconnect.app.core.firebase.FirebaseInitializer
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class CouplesConnectApp : Application() {
|
||||
|
||||
@Inject lateinit var firebaseInitializer: FirebaseInitializer
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Initialize app-wide components here
|
||||
firebaseInitializer.initialize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.couplesconnect.app.core.analytics
|
||||
|
||||
interface AnalyticsTracker {
|
||||
fun trackScreenViewed(screenName: String)
|
||||
fun trackAppOpened()
|
||||
fun trackQuestionViewed(questionId: String, category: String)
|
||||
fun trackAnswerSubmitted(questionId: String, category: String, answerType: String)
|
||||
fun trackAnswerRevealed(questionId: String, category: String)
|
||||
fun trackPaywallViewed(source: String)
|
||||
fun trackCouplePaired()
|
||||
fun trackCoupleLeft()
|
||||
fun trackSpinWheelStarted(categoryId: String)
|
||||
fun trackSpinWheelCompleted(categoryId: String, answeredCount: Int)
|
||||
fun trackSignIn(method: String)
|
||||
fun trackSignUp(method: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.couplesconnect.app.core.analytics
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.logEvent
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FirebaseAnalyticsTracker @Inject constructor(
|
||||
private val analytics: FirebaseAnalytics
|
||||
) : AnalyticsTracker {
|
||||
|
||||
override fun trackScreenViewed(screenName: String) {
|
||||
analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
|
||||
param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackAppOpened() {
|
||||
analytics.logEvent("app_opened", Bundle.EMPTY)
|
||||
}
|
||||
|
||||
override fun trackQuestionViewed(questionId: String, category: String) {
|
||||
analytics.logEvent("question_viewed") {
|
||||
param("question_id", questionId)
|
||||
param("category", category)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackAnswerSubmitted(questionId: String, category: String, answerType: String) {
|
||||
analytics.logEvent("answer_submitted") {
|
||||
param("question_id", questionId)
|
||||
param("category", category)
|
||||
param("answer_type", answerType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackAnswerRevealed(questionId: String, category: String) {
|
||||
analytics.logEvent("answer_revealed") {
|
||||
param("question_id", questionId)
|
||||
param("category", category)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackPaywallViewed(source: String) {
|
||||
analytics.logEvent("paywall_viewed") {
|
||||
param("source", source)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackCouplePaired() {
|
||||
analytics.logEvent("couple_paired", Bundle.EMPTY)
|
||||
}
|
||||
|
||||
override fun trackCoupleLeft() {
|
||||
analytics.logEvent("couple_left", Bundle.EMPTY)
|
||||
}
|
||||
|
||||
override fun trackSpinWheelStarted(categoryId: String) {
|
||||
analytics.logEvent("spin_wheel_started") {
|
||||
param("category_id", categoryId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackSpinWheelCompleted(categoryId: String, answeredCount: Int) {
|
||||
analytics.logEvent("spin_wheel_completed") {
|
||||
param("category_id", categoryId)
|
||||
param("answered_count", answeredCount.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackSignIn(method: String) {
|
||||
analytics.logEvent(FirebaseAnalytics.Event.LOGIN) {
|
||||
param(FirebaseAnalytics.Param.METHOD, method)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackSignUp(method: String) {
|
||||
analytics.logEvent(FirebaseAnalytics.Event.SIGN_UP) {
|
||||
param(FirebaseAnalytics.Param.METHOD, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.couplesconnect.app.core.crash
|
||||
|
||||
interface CrashReporter {
|
||||
fun setUserId(uid: String)
|
||||
fun clearUserId()
|
||||
fun log(message: String)
|
||||
fun recordException(throwable: Throwable)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.couplesconnect.app.core.crash
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FirebaseCrashReporter @Inject constructor() : CrashReporter {
|
||||
|
||||
private val crashlytics = FirebaseCrashlytics.getInstance()
|
||||
|
||||
override fun setUserId(uid: String) {
|
||||
crashlytics.setUserId(uid)
|
||||
}
|
||||
|
||||
override fun clearUserId() {
|
||||
crashlytics.setUserId("")
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
crashlytics.log(message)
|
||||
}
|
||||
|
||||
override fun recordException(throwable: Throwable) {
|
||||
crashlytics.recordException(throwable)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.couplesconnect.app.core.firebase
|
||||
|
||||
import com.couplesconnect.app.BuildConfig
|
||||
import com.google.firebase.appcheck.FirebaseAppCheck
|
||||
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FirebaseInitializer @Inject constructor() {
|
||||
|
||||
fun initialize() {
|
||||
val appCheck = FirebaseAppCheck.getInstance()
|
||||
if (BuildConfig.DEBUG) {
|
||||
// DebugAppCheckProviderFactory is in the debug artifact only;
|
||||
// referenced by name to avoid a compile-time dep in release.
|
||||
try {
|
||||
val cls = Class.forName(
|
||||
"com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory"
|
||||
)
|
||||
val factory = cls.getMethod("getInstance").invoke(null)
|
||||
as com.google.firebase.appcheck.AppCheckProviderFactory
|
||||
appCheck.installAppCheckProviderFactory(factory)
|
||||
} catch (_: Exception) {
|
||||
appCheck.installAppCheckProviderFactory(
|
||||
PlayIntegrityAppCheckProviderFactory.getInstance()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
appCheck.installAppCheckProviderFactory(
|
||||
PlayIntegrityAppCheckProviderFactory.getInstance()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.couplesconnect.app.di
|
||||
|
||||
import android.content.Context
|
||||
import com.couplesconnect.app.core.analytics.AnalyticsTracker
|
||||
import com.couplesconnect.app.core.analytics.FirebaseAnalyticsTracker
|
||||
import com.couplesconnect.app.core.crash.CrashReporter
|
||||
import com.couplesconnect.app.core.crash.FirebaseCrashReporter
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class ObservabilityModule {
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindAnalyticsTracker(impl: FirebaseAnalyticsTracker): AnalyticsTracker
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindCrashReporter(impl: FirebaseCrashReporter): CrashReporter
|
||||
|
||||
companion object {
|
||||
@Provides @Singleton
|
||||
fun provideFirebaseAnalytics(@ApplicationContext context: Context): FirebaseAnalytics =
|
||||
FirebaseAnalytics.getInstance(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function isSignedIn() {
|
||||
return request.auth != null;
|
||||
}
|
||||
|
||||
function isOwner(uid) {
|
||||
return isSignedIn() && request.auth.uid == uid;
|
||||
}
|
||||
|
||||
function isCouplesMember(coupleId) {
|
||||
return isSignedIn()
|
||||
&& request.auth.uid in
|
||||
get(/databases/$(database)/documents/couples/$(coupleId)).data.userIds;
|
||||
}
|
||||
|
||||
// ── Users ─────────────────────────────────────────────────────────────────
|
||||
// Each user owns exactly their own document.
|
||||
|
||||
match /users/{uid} {
|
||||
allow read, write: if isOwner(uid);
|
||||
}
|
||||
|
||||
// ── Invite codes ──────────────────────────────────────────────────────────
|
||||
// Any authenticated user can create or read an invite code.
|
||||
// Only status + acceptor fields may be updated (no re-writing the code).
|
||||
|
||||
match /invites/{code} {
|
||||
allow read: if isSignedIn();
|
||||
allow create: if isSignedIn();
|
||||
allow update: if isSignedIn()
|
||||
&& resource.data.status == 'pending'
|
||||
&& request.resource.data.keys().hasOnly(
|
||||
['status', 'acceptorUserId', 'acceptedAt', 'coupleId']);
|
||||
}
|
||||
|
||||
// ── Couples ───────────────────────────────────────────────────────────────
|
||||
// Only the two members of a couple may read or write couple data.
|
||||
|
||||
match /couples/{coupleId} {
|
||||
allow read, write: if isCouplesMember(coupleId);
|
||||
|
||||
// Question threads live under the couple document.
|
||||
match /question_threads/{threadId} {
|
||||
allow read, write: if isCouplesMember(coupleId);
|
||||
|
||||
// Answers: each user writes their own; both members can read all answers.
|
||||
match /answers/{userId} {
|
||||
allow write: if isOwner(userId);
|
||||
allow read: if isCouplesMember(coupleId);
|
||||
}
|
||||
|
||||
// Discussion messages: any couple member can write and read.
|
||||
match /messages/{messageId} {
|
||||
allow read, write: if isCouplesMember(coupleId);
|
||||
}
|
||||
|
||||
// Reactions: any couple member can write and read.
|
||||
match /reactions/{reactionId} {
|
||||
allow read, write: if isCouplesMember(coupleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue