Closer/app/build.gradle.kts

260 lines
9.9 KiB
Plaintext
Raw Normal View History

import java.util.Properties
plugins {
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)
}
// 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() }
android {
namespace = "app.closer"
compileSdk = 35
defaultConfig {
applicationId = "closer.app"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// 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.
buildConfigField(
"String",
"RC_API_KEY",
"\"${secret("RC_API_KEY") ?: "PLACEHOLDER_RC_API_KEY"}\""
)
}
buildFeatures {
buildConfig = true
compose = true
}
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
}
}
}
buildTypes {
debug {
// 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") ?: ""}\""
)
}
release {
buildConfigField("String", "APP_CHECK_DEBUG_TOKEN", "\"\"")
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
packaging {
resources.excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
}
testOptions {
unitTests {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}
}
// Abort any release assemble/bundle task when required credentials are absent.
// 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 {
val key = secret("RC_API_KEY")
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."
)
}
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."
)
}
}
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
// Compose
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)
// Navigation
implementation(libs.androidx.navigation.compose)
// ViewModel + lifecycle-aware Compose state collection (collectAsStateWithLifecycle)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
// Firebase
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)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
implementation(libs.androidx.hilt.navigation.compose)
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// DataStore
implementation(libs.androidx.datastore.preferences)
// Glance — home-screen "Today" widget (content-free: daily-question state + streak only)
implementation(libs.androidx.glance.appwidget)
implementation(libs.androidx.glance.material3)
// Encrypted storage
implementation(libs.androidx.security.crypto)
// Play Integrity API — runtime device integrity check
implementation(libs.google.play.integrity)
// Firebase Storage + Functions (callable for server-side integrity token verification)
implementation(libs.firebase.storage.ktx)
implementation(libs.firebase.functions.ktx)
// RevenueCat native Android SDK
implementation(libs.revenuecat.purchases)
// Image loading (+ animated GIF/WebP for stickers/Bitmoji)
implementation(libs.coil.compose)
implementation(libs.coil.gif)
// AppCompat (BiometricPrompt needs FragmentActivity) + biometric app-lock
implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric)
// Google Sign-In via Credential Manager
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.play.services.auth)
implementation(libs.google.identity.googleid)
// Coroutines
implementation(libs.kotlinx.coroutines.android)
// E2EE: Google Tink (AEAD) + Bouncy Castle (Argon2id KDF)
implementation(libs.tink.android)
implementation(libs.bouncycastle.bcprov)
// Debug
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
// Unit tests — JVM only (no device/emulator required)
testImplementation(libs.junit)
// Reflection over the RetentionEvent sealed hierarchy (NoPlaintextInAnalyticsTest schema check)
testImplementation(kotlin("reflect"))
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
// Real org.json on the JVM test classpath so codec round-trips run (Android's stubbed JSONObject
// returns defaults under unitTests.isReturnDefaultValues).
testImplementation(libs.org.json)
// Canonical-vector capture harness (paired-CI for iOS↔Android E2EE fixture fill)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.runner)
// 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)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
}