feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
|
|
|
import java.util.Properties
|
|
|
|
|
|
2026-06-15 18:43:43 -05:00
|
|
|
plugins {
|
2026-07-06 22:05:10 -05:00
|
|
|
alias(libs.plugins.android.application)
|
|
|
|
|
alias(libs.plugins.kotlin.android)
|
|
|
|
|
alias(libs.plugins.kotlin.compose)
|
|
|
|
|
alias(libs.plugins.google.services)
|
|
|
|
|
alias(libs.plugins.firebase.crashlytics)
|
|
|
|
|
alias(libs.plugins.hilt.android)
|
|
|
|
|
alias(libs.plugins.ksp)
|
2026-06-15 18:43:43 -05:00
|
|
|
}
|
|
|
|
|
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
|
|
|
// Secrets resolve from local.properties (gitignored) first, then -P/gradle.properties, then env.
|
|
|
|
|
// local.properties is NOT auto-loaded into Gradle properties, so read it explicitly here.
|
|
|
|
|
val localProperties = Properties().apply {
|
|
|
|
|
val file = rootProject.file("local.properties")
|
|
|
|
|
if (file.exists()) file.inputStream().use { load(it) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun secret(name: String): String? =
|
|
|
|
|
localProperties.getProperty(name)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: (project.findProperty(name) as? String)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: System.getenv(name)?.takeIf { it.isNotBlank() }
|
|
|
|
|
|
2026-06-15 18:43:43 -05:00
|
|
|
android {
|
2026-06-16 20:03:58 -05:00
|
|
|
namespace = "app.closer"
|
2026-06-15 18:43:43 -05:00
|
|
|
compileSdk = 35
|
|
|
|
|
|
|
|
|
|
defaultConfig {
|
2026-06-21 18:47:18 -05:00
|
|
|
applicationId = "closer.app"
|
2026-06-15 18:43:43 -05:00
|
|
|
minSdk = 26
|
|
|
|
|
targetSdk = 35
|
|
|
|
|
versionCode = 1
|
|
|
|
|
versionName = "0.1.0"
|
2026-06-17 01:22:24 -05:00
|
|
|
|
2026-06-29 13:01:08 -05:00
|
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
|
|
2026-06-20 01:10:20 -05:00
|
|
|
// RevenueCat API key. Set RC_API_KEY in local.properties (never committed).
|
|
|
|
|
// Debug builds fall back to a placeholder; release builds abort — see task guard below.
|
2026-06-17 01:22:24 -05:00
|
|
|
buildConfigField(
|
|
|
|
|
"String",
|
|
|
|
|
"RC_API_KEY",
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
|
|
|
"\"${secret("RC_API_KEY") ?: "PLACEHOLDER_RC_API_KEY"}\""
|
2026-06-17 01:22:24 -05:00
|
|
|
)
|
2026-06-15 18:43:43 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 01:57:48 -05:00
|
|
|
buildFeatures {
|
|
|
|
|
buildConfig = true
|
2026-06-16 20:16:47 -05:00
|
|
|
compose = true
|
2026-06-16 01:57:48 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 10:56:42 -05:00
|
|
|
signingConfigs {
|
|
|
|
|
create("release") {
|
|
|
|
|
// Set these four properties in local.properties (never commit) or as environment
|
|
|
|
|
// variables in CI. RELEASE_STORE_FILE is the path to the .jks / .keystore file,
|
|
|
|
|
// relative to the project root.
|
|
|
|
|
val storeFilePath = (findProperty("RELEASE_STORE_FILE") as? String)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: System.getenv("RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() }
|
|
|
|
|
val storePwd = (findProperty("RELEASE_STORE_PASSWORD") as? String)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: System.getenv("RELEASE_STORE_PASSWORD")?.takeIf { it.isNotBlank() }
|
|
|
|
|
val keyAliasVal = (findProperty("RELEASE_KEY_ALIAS") as? String)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: System.getenv("RELEASE_KEY_ALIAS")?.takeIf { it.isNotBlank() }
|
|
|
|
|
val keyPwd = (findProperty("RELEASE_KEY_PASSWORD") as? String)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: System.getenv("RELEASE_KEY_PASSWORD")?.takeIf { it.isNotBlank() }
|
|
|
|
|
|
|
|
|
|
if (storeFilePath != null && storePwd != null && keyAliasVal != null && keyPwd != null) {
|
|
|
|
|
storeFile = rootProject.file(storeFilePath)
|
|
|
|
|
storePassword = storePwd
|
|
|
|
|
keyAlias = keyAliasVal
|
|
|
|
|
keyPassword = keyPwd
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 18:43:43 -05:00
|
|
|
buildTypes {
|
2026-06-23 12:17:17 -05:00
|
|
|
debug {
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
|
|
|
// Stable debug token registered in Firebase Console > App Check, pre-seeded into
|
|
|
|
|
// SharedPreferences by FirebaseInitializer so all installs use the same token.
|
|
|
|
|
// NEVER hardcode it here — a committed token defeats App Check the moment
|
|
|
|
|
// enforcement is enabled. Set APP_CHECK_DEBUG_TOKEN in local.properties; when
|
|
|
|
|
// absent (e.g. CI), the field is empty and FirebaseInitializer skips seeding
|
|
|
|
|
// (each install then mints its own token, which won't be console-registered).
|
|
|
|
|
buildConfigField(
|
|
|
|
|
"String",
|
|
|
|
|
"APP_CHECK_DEBUG_TOKEN",
|
|
|
|
|
"\"${secret("APP_CHECK_DEBUG_TOKEN") ?: ""}\""
|
|
|
|
|
)
|
2026-06-23 12:17:17 -05:00
|
|
|
}
|
2026-06-15 18:43:43 -05:00
|
|
|
release {
|
2026-06-23 12:17:17 -05:00
|
|
|
buildConfigField("String", "APP_CHECK_DEBUG_TOKEN", "\"\"")
|
2026-06-23 10:56:42 -05:00
|
|
|
signingConfig = signingConfigs.getByName("release")
|
2026-06-16 20:16:47 -05:00
|
|
|
isMinifyEnabled = true
|
|
|
|
|
isShrinkResources = true
|
2026-06-15 18:43:43 -05:00
|
|
|
proguardFiles(
|
|
|
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
|
|
|
"proguard-rules.pro"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compileOptions {
|
|
|
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
|
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
kotlinOptions {
|
|
|
|
|
jvmTarget = "17"
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 21:08:55 -05:00
|
|
|
packaging {
|
|
|
|
|
resources.excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 22:51:08 -05:00
|
|
|
testOptions {
|
|
|
|
|
unitTests {
|
|
|
|
|
isReturnDefaultValues = true
|
|
|
|
|
isIncludeAndroidResources = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 22:59:38 -05:00
|
|
|
}
|
2026-06-16 20:16:47 -05:00
|
|
|
|
2026-06-23 10:56:42 -05:00
|
|
|
// Abort any release assemble/bundle task when required credentials are absent.
|
2026-06-20 01:10:20 -05:00
|
|
|
// This runs at execution time so debug builds are never affected.
|
|
|
|
|
tasks.matching { it.name.let { n ->
|
|
|
|
|
(n.startsWith("assemble") || n.startsWith("bundle")) && n.contains("Release", ignoreCase = true)
|
|
|
|
|
}}.configureEach {
|
|
|
|
|
doFirst {
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
|
|
|
val key = secret("RC_API_KEY")
|
2026-06-20 01:10:20 -05:00
|
|
|
if (key == null || key == "PLACEHOLDER_RC_API_KEY") {
|
|
|
|
|
throw GradleException(
|
|
|
|
|
"RC_API_KEY is not set. Add it to local.properties or export RC_API_KEY before running a release build."
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-06-23 10:56:42 -05:00
|
|
|
|
|
|
|
|
val storeFilePath = (findProperty("RELEASE_STORE_FILE") as? String)?.takeIf { it.isNotBlank() }
|
|
|
|
|
?: System.getenv("RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() }
|
|
|
|
|
if (storeFilePath == null) {
|
|
|
|
|
throw GradleException(
|
|
|
|
|
"Release signing is not configured. Set RELEASE_STORE_FILE, RELEASE_STORE_PASSWORD, " +
|
|
|
|
|
"RELEASE_KEY_ALIAS, and RELEASE_KEY_PASSWORD in local.properties or as environment variables."
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-06-20 01:10:20 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 22:59:38 -05:00
|
|
|
ksp {
|
|
|
|
|
arg("room.schemaLocation", "$projectDir/schemas")
|
2026-06-15 18:43:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dependencies {
|
2026-07-06 22:05:10 -05:00
|
|
|
val composeBom = platform(libs.androidx.compose.bom)
|
2026-06-15 18:43:43 -05:00
|
|
|
implementation(composeBom)
|
|
|
|
|
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.core.ktx)
|
|
|
|
|
implementation(libs.androidx.core.splashscreen)
|
|
|
|
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
|
|
|
implementation(libs.androidx.activity.compose)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
|
|
|
|
// Compose
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.compose.ui)
|
|
|
|
|
implementation(libs.androidx.compose.ui.graphics)
|
|
|
|
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
|
|
|
implementation(libs.androidx.compose.material3)
|
|
|
|
|
implementation(libs.androidx.compose.material.icons.extended)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
|
|
|
|
// Navigation
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.navigation.compose)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
2026-07-06 22:05:10 -05:00
|
|
|
// ViewModel + lifecycle-aware Compose state collection (collectAsStateWithLifecycle)
|
|
|
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
|
|
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
|
|
|
|
// Firebase
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(platform(libs.firebase.bom))
|
|
|
|
|
implementation(libs.firebase.auth.ktx)
|
|
|
|
|
implementation(libs.firebase.firestore.ktx)
|
|
|
|
|
implementation(libs.firebase.messaging.ktx)
|
|
|
|
|
implementation(libs.firebase.config.ktx)
|
|
|
|
|
implementation(libs.firebase.analytics.ktx)
|
|
|
|
|
implementation(libs.firebase.crashlytics.ktx)
|
|
|
|
|
implementation(libs.firebase.appcheck.ktx)
|
|
|
|
|
implementation(libs.firebase.appcheck.playintegrity)
|
|
|
|
|
debugImplementation(libs.firebase.appcheck.debug)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
|
|
|
|
// Hilt
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.hilt.android)
|
|
|
|
|
ksp(libs.hilt.android.compiler)
|
|
|
|
|
implementation(libs.androidx.hilt.navigation.compose)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
|
|
|
|
// Room
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.room.runtime)
|
|
|
|
|
implementation(libs.androidx.room.ktx)
|
|
|
|
|
ksp(libs.androidx.room.compiler)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
|
|
|
|
// DataStore
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.datastore.preferences)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
2026-07-06 20:40:28 -05:00
|
|
|
// Glance — home-screen "Today" widget (content-free: daily-question state + streak only)
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.glance.appwidget)
|
|
|
|
|
implementation(libs.androidx.glance.material3)
|
2026-07-06 20:40:28 -05:00
|
|
|
|
2026-06-16 20:16:47 -05:00
|
|
|
// Encrypted storage
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.security.crypto)
|
2026-06-16 20:16:47 -05:00
|
|
|
|
2026-06-17 19:37:19 -05:00
|
|
|
// Play Integrity API — runtime device integrity check
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.google.play.integrity)
|
2026-06-17 19:37:19 -05:00
|
|
|
|
2026-07-06 22:05:10 -05:00
|
|
|
// Firebase Storage + Functions (callable for server-side integrity token verification)
|
|
|
|
|
implementation(libs.firebase.storage.ktx)
|
|
|
|
|
implementation(libs.firebase.functions.ktx)
|
2026-06-17 19:37:19 -05:00
|
|
|
|
2026-07-06 22:05:10 -05:00
|
|
|
// RevenueCat native Android SDK
|
|
|
|
|
implementation(libs.revenuecat.purchases)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
2026-07-06 22:05:10 -05:00
|
|
|
// Image loading (+ animated GIF/WebP for stickers/Bitmoji)
|
|
|
|
|
implementation(libs.coil.compose)
|
|
|
|
|
implementation(libs.coil.gif)
|
2026-06-17 23:59:46 -05:00
|
|
|
|
2026-07-06 22:05:10 -05:00
|
|
|
// AppCompat (BiometricPrompt needs FragmentActivity) + biometric app-lock
|
|
|
|
|
implementation(libs.androidx.appcompat)
|
|
|
|
|
implementation(libs.androidx.biometric)
|
2026-06-21 16:27:55 -05:00
|
|
|
|
2026-06-17 23:02:25 -05:00
|
|
|
// Google Sign-In via Credential Manager
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.androidx.credentials)
|
|
|
|
|
implementation(libs.androidx.credentials.play.services.auth)
|
|
|
|
|
implementation(libs.google.identity.googleid)
|
2026-06-17 23:02:25 -05:00
|
|
|
|
2026-06-15 18:43:43 -05:00
|
|
|
// Coroutines
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.kotlinx.coroutines.android)
|
2026-06-15 18:43:43 -05:00
|
|
|
|
feat: E2EE — Tink AEAD, Argon2id KDF, recovery phrase, encrypted Firestore fields (batch v0.2.6)
- Add crypto module: CoupleKeyStore (EncryptedSharedPreferences), RecoveryKeyManager (Argon2id + AES-256-GCM key wrap), FieldEncryptor (AEAD per-field), CoupleEncryptionManager (orchestration)
- Add Tink + Bouncy Castle dependencies to build.gradle.kts, register AeadConfig in CloserApp
- Encrypt answer fields (writtenText, selectedOptionIds, scaleValue) on write, decrypt on read
- Encrypt DesireSync, HowWell, WheelAnswer, QuestionThread fields via CoupleEncryptionManager
- Generate recovery phrase during invite creation, display in CreateInviteScreen
- Add recovery phrase input to InviteConfirmScreen for encrypted invites
- Add RecoveryScreen + RecoveryViewModel for post-pairing key recovery
- Update Couple model with encryptionVersion, wrappedCoupleKey, kdfSalt, kdfParams
- Update Firestore rules: allow couple doc creation by members, fcmTokens path, encryptionVersion monotonic check, invite doc extended fields
2026-06-19 19:52:35 -05:00
|
|
|
// E2EE: Google Tink (AEAD) + Bouncy Castle (Argon2id KDF)
|
2026-07-06 22:05:10 -05:00
|
|
|
implementation(libs.tink.android)
|
|
|
|
|
implementation(libs.bouncycastle.bcprov)
|
feat: E2EE — Tink AEAD, Argon2id KDF, recovery phrase, encrypted Firestore fields (batch v0.2.6)
- Add crypto module: CoupleKeyStore (EncryptedSharedPreferences), RecoveryKeyManager (Argon2id + AES-256-GCM key wrap), FieldEncryptor (AEAD per-field), CoupleEncryptionManager (orchestration)
- Add Tink + Bouncy Castle dependencies to build.gradle.kts, register AeadConfig in CloserApp
- Encrypt answer fields (writtenText, selectedOptionIds, scaleValue) on write, decrypt on read
- Encrypt DesireSync, HowWell, WheelAnswer, QuestionThread fields via CoupleEncryptionManager
- Generate recovery phrase during invite creation, display in CreateInviteScreen
- Add recovery phrase input to InviteConfirmScreen for encrypted invites
- Add RecoveryScreen + RecoveryViewModel for post-pairing key recovery
- Update Couple model with encryptionVersion, wrappedCoupleKey, kdfSalt, kdfParams
- Update Firestore rules: allow couple doc creation by members, fcmTokens path, encryptionVersion monotonic check, invite doc extended fields
2026-06-19 19:52:35 -05:00
|
|
|
|
2026-06-15 18:43:43 -05:00
|
|
|
// Debug
|
2026-07-06 22:05:10 -05:00
|
|
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
|
|
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
2026-06-17 21:08:13 -05:00
|
|
|
|
|
|
|
|
// Unit tests — JVM only (no device/emulator required)
|
2026-07-06 22:05:10 -05:00
|
|
|
testImplementation(libs.junit)
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
|
|
|
// Reflection over the RetentionEvent sealed hierarchy (NoPlaintextInAnalyticsTest schema check)
|
|
|
|
|
testImplementation(kotlin("reflect"))
|
2026-07-06 22:05:10 -05:00
|
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
|
|
|
testImplementation(libs.mockk)
|
2026-06-30 20:43:30 -05:00
|
|
|
// Real org.json on the JVM test classpath so codec round-trips run (Android's stubbed JSONObject
|
|
|
|
|
// returns defaults under unitTests.isReturnDefaultValues).
|
2026-07-06 22:05:10 -05:00
|
|
|
testImplementation(libs.org.json)
|
2026-06-28 17:56:44 -05:00
|
|
|
|
|
|
|
|
// Canonical-vector capture harness (paired-CI for iOS↔Android E2EE fixture fill)
|
2026-07-06 22:05:10 -05:00
|
|
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
|
|
|
|
androidTestImplementation(libs.androidx.test.runner)
|
2026-06-29 13:01:08 -05:00
|
|
|
|
|
|
|
|
// Instrumented Compose render smoke (first-run screens) — the on-device net for the
|
|
|
|
|
// "composes fine, crashes on first paint" class (e.g. O-ONBOARD-001). Needs the BOM so the
|
|
|
|
|
// ui-test version matches the app's Compose; ui-test-manifest (debug, above) hosts the ComposeRule.
|
|
|
|
|
androidTestImplementation(composeBom)
|
2026-07-06 22:05:10 -05:00
|
|
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
2026-06-15 18:43:43 -05:00
|
|
|
}
|