apply plugin: 'com.android.application' def keystoreProps = new Properties() def keystoreFile = rootProject.file('keystore.properties') if (keystoreFile.exists()) keystoreProps.load(new FileInputStream(keystoreFile)) // COINOS_UNSIGNED=1 builds without the keystore even when it is present — // the reproducible-build check compares two unsigned builds byte for byte, // and F-Droid builds unsigned and compares against our signed release. def signRelease = keystoreFile.exists() && System.getenv('COINOS_UNSIGNED') == null // The bundled shell carries the built web app as assets. It is copied from // ../dist, which the web build produces first: COINOS_NO_SW=1 bun run build // (the service worker is left out — inside the WebView the app is served // from these assets, and a worker would fetch the live site over it). tasks.register('copyWeb', Copy) { def dist = rootProject.file('../dist') // the repo root's dist, not the module's doFirst { if (!new File(dist, 'index.html').exists()) throw new GradleException('build the web app first: COINOS_NO_SW=1 bun run build (in the repo root)') } from(dist) { exclude '*.apk', 'standalone.html', 'sw.js', 'zmq-watcher.js' } into layout.buildDirectory.dir('www') } // Firebase settings for the fcm build, read from app/google-services.json // (downloaded from the Firebase console; gitignored). Without it the fcm // build still builds, and pushes through UnifiedPush like the webview one. def firebase = [appId: '', apiKey: '', projectId: '', senderId: ''] def gsFile = file('google-services.json') if (gsFile.exists()) { def gs = new groovy.json.JsonSlurper().parse(gsFile) def client = gs.client.find { it.client_info?.android_client_info?.package_name == 'io.coinos.beta' } if (client) { firebase = [ appId: client.client_info.mobilesdk_app_id, apiKey: client.api_key[0].current_key, projectId: gs.project_info.project_id, senderId: gs.project_info.project_number, ] } } android { namespace 'io.coinos.app' compileSdk 35 defaultConfig { applicationId 'io.coinos.app' minSdk 24 targetSdk 35 versionCode 15 versionName '1.14' } // Two builds of the same wallet. // // twa — Chrome renders the site (the better browser, and the one // people already have installed). Its storage is Chrome's, so // nothing outside Chrome can wake the wallet, and on a phone // without Google Play Services nothing inside it can either. // webview — the wallet runs in the app, so a UnifiedPush message can // start it off-screen and let it answer. A separate app id, // because it is a separate wallet: its storage is its own. // fdroid — the webview shell with the web app BUNDLED as assets: what // runs is what was built from this source tree, reproducibly, // which is what F-Droid publishes. No service worker, no // remote code; the live site is only reached for its APIs. // Google's dependency-metadata block (a Play-only extra in the APK // signing block, encrypted to Google) is refused by F-Droid's scanner. dependenciesInfo { includeInApk = false includeInBundle = false } flavorDimensions += 'shell' buildFeatures { buildConfig true } productFlavors { twa { dimension 'shell' isDefault = true buildConfigField 'boolean', 'BUNDLED', 'false' } webview { dimension 'shell' applicationId 'io.coinos.webview' versionNameSuffix '-webview' resValue 'string', 'app_name', 'Coinos (UnifiedPush)' buildConfigField 'boolean', 'BUNDLED', 'false' } // fcm — the webview shell plus Firebase Cloud Messaging: push without a // distributor app on phones with Google Play Services (see // src/fcm/java/io/coinos/app/Fcm.java). Its own app id, so it // installs beside the TWA for a side-by-side trial. fcm { dimension 'shell' applicationId 'io.coinos.beta' versionNameSuffix '-beta' resValue 'string', 'app_name', 'Coinos Beta' buildConfigField 'boolean', 'BUNDLED', 'false' buildConfigField 'String', 'FCM_APP_ID', "\"${firebase.appId}\"" buildConfigField 'String', 'FCM_API_KEY', "\"${firebase.apiKey}\"" buildConfigField 'String', 'FCM_PROJECT_ID', "\"${firebase.projectId}\"" buildConfigField 'String', 'FCM_SENDER_ID', "\"${firebase.senderId}\"" } fdroid { dimension 'shell' applicationId 'io.coinos.fdroid' resValue 'string', 'app_name', 'Coinos' buildConfigField 'boolean', 'BUNDLED', 'true' } } sourceSets { // the webview shell has no Firebase: its Fcm is a stub webview { java.srcDirs += 'src/nofcm/java' } // the webview shell with Firebase (the manifest is webview's plus FCM) fcm { java.srcDirs = ['src/webview/java', 'src/fcm/java'] res.srcDirs = ['src/webview/res'] manifest.srcFile 'src/fcm/AndroidManifest.xml' } // the same shell as webview, plus the bundled web app fdroid { java.srcDirs = ['src/webview/java', 'src/nofcm/java'] res.srcDirs = ['src/webview/res'] manifest.srcFile 'src/webview/AndroidManifest.xml' assets.srcDirs += layout.buildDirectory.dir('www').get().asFile } } signingConfigs { release { if (signRelease) { storeFile rootProject.file(keystoreProps['storeFile']) storePassword keystoreProps['storePassword'] keyAlias keystoreProps['keyAlias'] keyPassword keystoreProps['keyPassword'] } } } buildTypes { release { minifyEnabled false if (signRelease) signingConfig signingConfigs.release } } } dependencies { // only the TWA shell needs Chrome's helper; the WebView build carries none twaImplementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0' // WebAuthn (passkeys) inside the WebView builds — androidx, no Google services webviewImplementation 'androidx.webkit:webkit:1.14.0' fcmImplementation 'androidx.webkit:webkit:1.14.0' fdroidImplementation 'androidx.webkit:webkit:1.14.0' // only the fcm build carries Google's push client fcmImplementation 'com.google.firebase:firebase-messaging:24.1.0' } preBuild.dependsOn copyWeb