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-config-ktx")
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||||
implementation("com.google.firebase:firebase-crashlytics-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
|
// Hilt
|
||||||
implementation("com.google.dagger:hilt-android:2.53.1")
|
implementation("com.google.dagger:hilt-android:2.53.1")
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
package com.couplesconnect.app
|
package com.couplesconnect.app
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import com.couplesconnect.app.core.firebase.FirebaseInitializer
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class CouplesConnectApp : Application() {
|
class CouplesConnectApp : Application() {
|
||||||
|
|
||||||
|
@Inject lateinit var firebaseInitializer: FirebaseInitializer
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.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