260 lines
9.9 KiB
Plaintext
260 lines
9.9 KiB
Plaintext
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)
|
|
}
|