diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1278981..b3e53ed2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,7 +57,14 @@ android { } buildTypes { + debug { + // Stable debug token registered in Firebase Console > App Check. + // Pre-seeded into SharedPreferences by FirebaseInitializer so all installs + // use the same token without manual re-registration. + buildConfigField("String", "APP_CHECK_DEBUG_TOKEN", "\"e2dc8256-403f-449b-846e-76614a7297cc\"") + } release { + buildConfigField("String", "APP_CHECK_DEBUG_TOKEN", "\"\"") signingConfig = signingConfigs.getByName("release") isMinifyEnabled = true isShrinkResources = true diff --git a/app/src/main/java/app/closer/core/firebase/FirebaseInitializer.kt b/app/src/main/java/app/closer/core/firebase/FirebaseInitializer.kt index 9a7099ee..cfacbf66 100644 --- a/app/src/main/java/app/closer/core/firebase/FirebaseInitializer.kt +++ b/app/src/main/java/app/closer/core/firebase/FirebaseInitializer.kt @@ -1,19 +1,25 @@ package app.closer.core.firebase -import app.closer.BuildConfig // generated by buildFeatures { buildConfig = true } +import android.content.Context +import app.closer.BuildConfig +import com.google.firebase.FirebaseApp import com.google.firebase.appcheck.FirebaseAppCheck import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory +import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton -class FirebaseInitializer @Inject constructor() { +class FirebaseInitializer @Inject constructor( + @ApplicationContext private val context: Context +) { 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. + // Pre-seed the stable registered debug token so every install (emulator or device) + // uses the same token without needing re-registration in Firebase Console. + seedDebugToken() try { val cls = Class.forName( "com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory" @@ -32,4 +38,19 @@ class FirebaseInitializer @Inject constructor() { ) } } + + private fun seedDebugToken() { + val token = BuildConfig.APP_CHECK_DEBUG_TOKEN + if (token.isBlank()) return + try { + val persistenceKey = FirebaseApp.getInstance().persistenceKey + val prefsFile = "com.google.firebase.appcheck.debug.store.$persistenceKey" + context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE) + .edit() + .putString("com.google.firebase.appcheck.debug.DEBUG_SECRET", token) + .commit() + } catch (_: Exception) { + // Best effort — debug provider will generate and log a new token if this fails. + } + } } diff --git a/firestore.indexes.json b/firestore.indexes.json index 3124c680..529da8d7 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -15,6 +15,22 @@ { "fieldPath": "status", "order": "ASCENDING" }, { "fieldPath": "unlockAt", "order": "ASCENDING" } ] + }, + { + "collectionGroup": "questions", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "active", "order": "ASCENDING" }, + { "fieldPath": "isPremium", "order": "ASCENDING" } + ] + }, + { + "collectionGroup": "bucket_list", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "category", "order": "ASCENDING" }, + { "fieldPath": "addedAt", "order": "DESCENDING" } + ] } ], "fieldOverrides": [ diff --git a/iphone/Closer/Theme/CloserTheme.swift b/iphone/Closer/Theme/CloserTheme.swift index 36bdce9e..7f5ce75d 100644 --- a/iphone/Closer/Theme/CloserTheme.swift +++ b/iphone/Closer/Theme/CloserTheme.swift @@ -3,32 +3,38 @@ import SwiftUI // MARK: - Theme extension Color { + private static func adaptive(light: String, dark: String) -> Color { + Color(UIColor { traitCollection in + UIColor(hex: traitCollection.userInterfaceStyle == .dark ? dark : light) + }) + } + // Primary palette - static let closerPrimary = Color(hex: "B98AF4") - static let closerSecondary = Color(hex: "E7A2D1") - static let closerBackground = Color(hex: "FFFBFE") - static let closerSurface = Color(hex: "F5F0FF") + static let closerPrimary = Color.adaptive(light: "B98AF4", dark: "CFA7FF") + static let closerSecondary = Color.adaptive(light: "E7A2D1", dark: "FFAFD9") + static let closerBackground = Color.adaptive(light: "FFFBFE", dark: "18111E") + static let closerSurface = Color.adaptive(light: "F5F0FF", dark: "211729") static let closerOnPrimary = Color.white - static let closerText = Color(hex: "1C1B1F") - static let closerTextSecondary = Color(hex: "49454F") - static let closerDivider = Color(hex: "E6E0E9") + static let closerText = Color.adaptive(light: "1C1B1F", dark: "F2E8F6") + static let closerTextSecondary = Color.adaptive(light: "49454F", dark: "D9C8E2") + static let closerDivider = Color.adaptive(light: "E6E0E9", dark: "5A4666") // Semantic - static let closerSuccess = Color(hex: "4CAF50") - static let closerWarning = Color(hex: "FF9800") - static let closerDanger = Color(hex: "F44336") - static let closerGold = Color(hex: "FFD700") + static let closerSuccess = Color.adaptive(light: "4CAF50", dark: "8DD99B") + static let closerWarning = Color.adaptive(light: "FF9800", dark: "FFC46B") + static let closerDanger = Color.adaptive(light: "F44336", dark: "FFB3BA") + static let closerGold = Color.adaptive(light: "FFD700", dark: "FFE680") // Category colors - static let categoryCommunication = Color(hex: "B98AF4") - static let categoryIntimacy = Color(hex: "E7A2D1") - static let categoryFun = Color(hex: "FFB74D") - static let categoryGoals = Color(hex: "81C784") - static let categoryAdventure = Color(hex: "64B5F6") + static let categoryCommunication = Color.adaptive(light: "B98AF4", dark: "CFA7FF") + static let categoryIntimacy = Color.adaptive(light: "E7A2D1", dark: "FFAFD9") + static let categoryFun = Color.adaptive(light: "FFB74D", dark: "FFD38A") + static let categoryGoals = Color.adaptive(light: "81C784", dark: "A6DFA8") + static let categoryAdventure = Color.adaptive(light: "64B5F6", dark: "9AD1FF") // Streak - static let streakActive = Color(hex: "FF6B6B") - static let streakInactive = Color(hex: "E0E0E0") + static let streakActive = Color.adaptive(light: "FF6B6B", dark: "FF9A9A") + static let streakInactive = Color.adaptive(light: "E0E0E0", dark: "5A4666") init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) @@ -53,6 +59,29 @@ extension Color { } } +private extension UIColor { + convenience init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 6: + (a, r, g, b) = (255, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF) + case 8: + (a, r, g, b) = ((int >> 24) & 0xFF, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init( + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + alpha: Double(a) / 255 + ) + } +} + // MARK: - Typography enum CloserFont {