import org.jetbrains.kotlin.gradle.dsl.JvmTarget /** * API origin, baked in per build type. Must end in a slash - Retrofit resolves relative paths * against it. * * `cashloop.apiBaseUrl` affects the **debug build only**. Set it in gradle.properties and press Run * in Android Studio, or pass it per-invocation: * * ./gradlew :app:installDebug -Pcashloop.apiBaseUrl=https://abc123.ngrok.io/ * * It used to feed the release URL as well, which is how a personal ngrok tunnel ended up compiled * into a published APK: a value left in the committed gradle.properties silently redirected every * release build too, and the hostname carried the developer's IP address. * * Overriding the release origin is now a separate, deliberately awkward property that nothing sets * by accident. The ordinary way to change it is to edit the default below. */ val debugApiBaseUrl: String = (findProperty("cashloop.apiBaseUrl") as String?) ?: "http://10.0.2.2:8000/" val releaseApiBaseUrl: String = (findProperty("cashloop.releaseApiBaseUrl") as String?) ?: "https://panel.cashloop.app/" /** * Reads a release signing value from a Gradle property, falling back to an environment variable. * * Both live outside the repository on purpose: a keystore path or password committed here is a * published credential, and the whole point of the release config is that only you hold the key. */ fun releaseSigningProperty(propertyName: String, environmentName: String): String? = (findProperty(propertyName) as String?)?.takeIf { it.isNotBlank() } ?: System.getenv(environmentName)?.takeIf { it.isNotBlank() } plugins { // Kotlin support is built into AGP 9 — the 'org.jetbrains.kotlin.android' plugin must NOT be // applied. https://developer.android.com/r/tools/built-in-kotlin alias(libs.plugins.android.application) alias(libs.plugins.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.hilt) alias(libs.plugins.gms) } android { namespace = "com.cashparty.rewards.app" compileSdk = 36 defaultConfig { applicationId = "com.cashparty.rewards.app" // 26 rather than 24: the Arcade type scale drives Fredoka's and Plus Jakarta Sans' // `wght` axis through FontVariation, which the platform ignores below API 26 - every // weight would silently render at the axis default. minSdk = 26 targetSdk = 36 versionCode = 12 versionName = "2.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true } signingConfigs { // Bundled, so debug builds from any checkout stay upgradable over each other. // Never used for release - see below. named("debug") { storeFile = rootProject.file("debug.keystore") storePassword = "android" keyAlias = "androiddebugkey" keyPassword = "android" } /* * Your own release key, supplied from outside the repository. * * Set these in ~/.gradle/gradle.properties (never in the project - that file is * committed) or as environment variables in CI: * * cashloop.releaseStoreFile=/absolute/path/to/release.jks * cashloop.releaseStorePassword=... * cashloop.releaseKeyAlias=... * cashloop.releaseKeyPassword=... * * Create one with: * keytool -genkeypair -v -keystore release.jks -keyalg RSA \ * -keysize 2048 -validity 10000 -alias cashloop * * Keep the file and its passwords backed up: Play will not accept an update signed * with a different key, and there is no recovery. */ create("release") { val storePath = releaseSigningProperty("cashloop.releaseStoreFile", "CASHLOOP_RELEASE_STORE_FILE") if (storePath != null) { storeFile = file(storePath) storePassword = releaseSigningProperty("cashloop.releaseStorePassword", "CASHLOOP_RELEASE_STORE_PASSWORD") keyAlias = releaseSigningProperty("cashloop.releaseKeyAlias", "CASHLOOP_RELEASE_KEY_ALIAS") keyPassword = releaseSigningProperty("cashloop.releaseKeyPassword", "CASHLOOP_RELEASE_KEY_PASSWORD") } } } buildTypes { debug { signingConfig = signingConfigs.named("debug").get() // 10.0.2.2 is the emulator's route to the host machine - `localhost` inside the // emulator is the emulator. On a physical device, override with `-Pcashloop.apiBaseUrl=` // pointing at your LAN IP or a tunnel; Remote Config's `api_base_url` overrides both // at runtime without a rebuild. buildConfigField("String", "API_BASE_URL", "\"$debugApiBaseUrl\"") } release { buildConfigField("String", "API_BASE_URL", "\"$releaseApiBaseUrl\"") // R8 shrinks, obfuscates, and strips android.util.Log through the // -assumenosideeffects rule in proguard-rules.pro. With this off none of that // happens: the rules file is inert and the published APK carries readable // class names and every debug log. isMinifyEnabled = true isShrinkResources = true /* * Deliberately NOT the debug keystore. * * debug.keystore ships inside this project, so anyone holding the source holds the * key. Signing a release with it means a third party can build an APK the device * accepts as an update to yours. * * With no release key configured the variant builds unsigned - `assembleRelease` * succeeds and produces an -unsigned.apk, but `bundleRelease` output cannot be * uploaded until you supply your own key above. */ signingConfig = signingConfigs.findByName("release")?.takeIf { it.storeFile != null } proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } compileOptions { // Up to Java 11 APIs are available through desugaring // https://developer.android.com/studio/write/java11-minimal-support-table sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 isCoreLibraryDesugaringEnabled = true } buildFeatures { compose = true buildConfig = true } packaging { resources { excludes.add("/META-INF/{AL2.0,LGPL2.1}") } } testOptions { animationsDisabled = true unitTests.isIncludeAndroidResources = true // `android.util.Log` throws "not mocked" by default, so any code path that logs is // untestable on the JVM - including every error path in `apiCall`, which is precisely // what these tests exist to cover. unitTests.isReturnDefaultValues = true } } kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_11 freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi") } } dependencies { implementation(projects.core.datastoreProto) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) androidTestImplementation(composeBom) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.core.ktx) implementation(libs.androidx.palette) implementation(libs.androidx.multidex) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.animation) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.lifecycle.livedataKtx) implementation(libs.androidx.lifecycle.runtimeKtx) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.dataStore) implementation(libs.androidx.dataStore.preferences) implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) ksp(libs.hilt.compiler) ksp(libs.kotlin.metadata) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.auth) implementation(libs.firebase.cloud.messaging) implementation(libs.firebase.config) implementation(libs.retrofit.core) implementation(libs.retrofit.kotlin.serialization) implementation(libs.okhttp) implementation(libs.okhttp.logging) // Offerwall SDK. Opens the wall only - rewards arrive by server-to-server postback, // never through the SDK, so nothing here touches a wallet. implementation(libs.bitlabs.core) implementation(libs.ayet.sdk) implementation(libs.mychips.offerwall) implementation(libs.fyber.fairbid) implementation(libs.coil.kt.compose) implementation(libs.coil.kt.svg) implementation(libs.lottie.compose) implementation(libs.shimmer.compose) implementation(libs.play.services.ads) implementation(libs.play.services.auth) implementation(libs.facebook.login) implementation(libs.unity.ads) implementation(libs.onesignal) // Tracked offer/partner URLs open in a Custom Tab: the click id in that URL is what the // partner's postback returns, so the link must be opened as given rather than handed to a // store intent that would drop the query string. implementation(libs.androidx.browser) coreLibraryDesugaring(libs.android.desugarJdkLibs) testImplementation(libs.junit) testImplementation(libs.kotlin.test) testImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.androidx.test.ext) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.hilt.android.testing) kspAndroidTest(libs.hilt.compiler) debugImplementation(libs.androidx.compose.ui.testManifest) }