feat(app-check): stable debug token via BuildConfig; feat(firestore): indexes for questions + bucket_list

This commit is contained in:
null 2026-06-23 12:17:17 -05:00
parent 6977db7600
commit e5c13b6b6d
4 changed files with 95 additions and 22 deletions

View File

@ -57,7 +57,14 @@ android {
} }
buildTypes { 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 { release {
buildConfigField("String", "APP_CHECK_DEBUG_TOKEN", "\"\"")
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true

View File

@ -1,19 +1,25 @@
package app.closer.core.firebase 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.FirebaseAppCheck
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class FirebaseInitializer @Inject constructor() { class FirebaseInitializer @Inject constructor(
@ApplicationContext private val context: Context
) {
fun initialize() { fun initialize() {
val appCheck = FirebaseAppCheck.getInstance() val appCheck = FirebaseAppCheck.getInstance()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// DebugAppCheckProviderFactory is in the debug artifact only; // Pre-seed the stable registered debug token so every install (emulator or device)
// referenced by name to avoid a compile-time dep in release. // uses the same token without needing re-registration in Firebase Console.
seedDebugToken()
try { try {
val cls = Class.forName( val cls = Class.forName(
"com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory" "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.
}
}
} }

View File

@ -15,6 +15,22 @@
{ "fieldPath": "status", "order": "ASCENDING" }, { "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "unlockAt", "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": [ "fieldOverrides": [

View File

@ -3,32 +3,38 @@ import SwiftUI
// MARK: - Theme // MARK: - Theme
extension Color { extension Color {
private static func adaptive(light: String, dark: String) -> Color {
Color(UIColor { traitCollection in
UIColor(hex: traitCollection.userInterfaceStyle == .dark ? dark : light)
})
}
// Primary palette // Primary palette
static let closerPrimary = Color(hex: "B98AF4") static let closerPrimary = Color.adaptive(light: "B98AF4", dark: "CFA7FF")
static let closerSecondary = Color(hex: "E7A2D1") static let closerSecondary = Color.adaptive(light: "E7A2D1", dark: "FFAFD9")
static let closerBackground = Color(hex: "FFFBFE") static let closerBackground = Color.adaptive(light: "FFFBFE", dark: "18111E")
static let closerSurface = Color(hex: "F5F0FF") static let closerSurface = Color.adaptive(light: "F5F0FF", dark: "211729")
static let closerOnPrimary = Color.white static let closerOnPrimary = Color.white
static let closerText = Color(hex: "1C1B1F") static let closerText = Color.adaptive(light: "1C1B1F", dark: "F2E8F6")
static let closerTextSecondary = Color(hex: "49454F") static let closerTextSecondary = Color.adaptive(light: "49454F", dark: "D9C8E2")
static let closerDivider = Color(hex: "E6E0E9") static let closerDivider = Color.adaptive(light: "E6E0E9", dark: "5A4666")
// Semantic // Semantic
static let closerSuccess = Color(hex: "4CAF50") static let closerSuccess = Color.adaptive(light: "4CAF50", dark: "8DD99B")
static let closerWarning = Color(hex: "FF9800") static let closerWarning = Color.adaptive(light: "FF9800", dark: "FFC46B")
static let closerDanger = Color(hex: "F44336") static let closerDanger = Color.adaptive(light: "F44336", dark: "FFB3BA")
static let closerGold = Color(hex: "FFD700") static let closerGold = Color.adaptive(light: "FFD700", dark: "FFE680")
// Category colors // Category colors
static let categoryCommunication = Color(hex: "B98AF4") static let categoryCommunication = Color.adaptive(light: "B98AF4", dark: "CFA7FF")
static let categoryIntimacy = Color(hex: "E7A2D1") static let categoryIntimacy = Color.adaptive(light: "E7A2D1", dark: "FFAFD9")
static let categoryFun = Color(hex: "FFB74D") static let categoryFun = Color.adaptive(light: "FFB74D", dark: "FFD38A")
static let categoryGoals = Color(hex: "81C784") static let categoryGoals = Color.adaptive(light: "81C784", dark: "A6DFA8")
static let categoryAdventure = Color(hex: "64B5F6") static let categoryAdventure = Color.adaptive(light: "64B5F6", dark: "9AD1FF")
// Streak // Streak
static let streakActive = Color(hex: "FF6B6B") static let streakActive = Color.adaptive(light: "FF6B6B", dark: "FF9A9A")
static let streakInactive = Color(hex: "E0E0E0") static let streakInactive = Color.adaptive(light: "E0E0E0", dark: "5A4666")
init(hex: String) { init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 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 // MARK: - Typography
enum CloserFont { enum CloserFont {