diff --git a/AGENTS.md b/AGENTS.md index 0a37d84c..b6152d5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,16 +2,18 @@ ## Android Build — Google Play Services / FOSS Compatibility -**Firebase is opt-in. Do NOT enable it accidentally.** +**Firebase is on by default for Android and iOS builds.** Do not turn it off unless the build is an exceptional FOSS/F-Droid artifact. -`android/google-services.json` is gitignored and may be present on disk for push notification development, but Firebase is only activated when you explicitly pass `-PfirebaseEnabled` to Gradle: +`android/app/google-services.json` is required when Firebase is enabled. It does not decide the switch: a missing file fails the build, and a file on disk is ignored when Firebase is explicitly disabled. -- **FOSS / APK / Aurora / Zapstore / F-Droid builds**: just `./gradlew assembleRelease` — Firebase stays off even if `google-services.json` is on disk. -- **Firebase / FCM / Play Store builds**: `./gradlew bundleRelease -PfirebaseEnabled` — explicitly opt in. +- **Default (local, test, production, Play Store)**: `./gradlew assembleRelease` or `./gradlew bundleRelease` — do not pass `-PfirebaseEnabled`. +- **Opt out (F-Droid / FOSS)**: `./gradlew assembleRelease -PfirebaseEnabled=false`. npm scripts honor `FIREBASE_ENABLED=false`, which forwards that property. -This guard is in `android/app/build.gradle`. Do NOT change this conditional to activate Firebase unconditionally based on file presence alone — that was the bug that broke FOSS distribution in June 2026. +iOS builds bundle `ios/App/App/GoogleService-Info.plist` by default (`ios/App/scripts/bundle-firebase-config.sh`, run from `scripts/build-ios.sh` and as an Xcode build phase). Opt out with `FIREBASE_ENABLED=false`. A missing plist fails the build; the file's presence does not enable Firebase on its own. -`google-services.json` is intentionally excluded from git (`android/.gitignore`). Never commit it. +The guard is in `android/app/build.gradle`. Do NOT activate Firebase from file presence alone — that was the bug that broke FOSS distribution in June 2026. + +Never commit `google-services.json`. Full details, incident history, and F-Droid notes: `doc/android-firebase-gms.md` diff --git a/BUILDING.md b/BUILDING.md index a6f618b8..181572f4 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1474,9 +1474,11 @@ So, to create a signed `aab` release: ```bash # if you use pkgx: pkgx +openjdk.org /bin/zsh -./gradlew bundleRelease -Dlint.baselines.continue=true -PfirebaseEnabled +./gradlew bundleRelease -Dlint.baselines.continue=true ``` +Firebase is on by default (`firebaseEnabled=true` in `android/gradle.properties`). Do not pass `-PfirebaseEnabled` for a normal release. F-Droid and other builds that must omit Firebase use `-PfirebaseEnabled=false`. + ... and find your `aab` file at app/build/outputs/bundle/release * Gradle packages whatever web bundle was last synced into @@ -1486,8 +1488,6 @@ So, to create a signed `aab` release: * `build-android.sh` runs `./gradlew clean`, which deletes any earlier `aab`, so run `bundleRelease` after the build script finishes. -* Note that F-Droid builds should omit `-PfirebaseEnabled`. - At play.google.com/console: - Go to Production or the Closed Testing and either Create Track or Manage Track. diff --git a/android/app/build.gradle b/android/app/build.gradle index 3fa73886..bf67d857 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -130,20 +130,32 @@ dependencies { apply from: 'capacitor.build.gradle' -// Firebase / Google Play Services are opt-in. Pass -PfirebaseEnabled to any Gradle command -// to activate Firebase (FCM push notifications). Without this flag the build works on -// F-Droid, Aurora, Zapstore, and plain APK sideloading even when google-services.json -// is present on disk (it is gitignored; see AGENTS.md for the full story). -try { - def servicesJSON = file('google-services.json') - if (servicesJSON.exists() && servicesJSON.text && project.hasProperty('firebaseEnabled')) { - apply plugin: 'com.google.gms.google-services' - logger.info("Firebase enabled: google-services plugin applied") - } else if (servicesJSON.exists() && !project.hasProperty('firebaseEnabled')) { - logger.info("google-services.json present but firebaseEnabled not set — skipping Firebase plugin (pass -PfirebaseEnabled to enable)") - } else { - logger.info("google-services.json not found — Firebase plugin not applied") +// Firebase is on for every build. Opt out only with -PfirebaseEnabled=false +// (F-Droid / other builds that must not apply the google-services plugin). +// The config file does not decide the switch: when Firebase is on, a missing +// file fails the build; when it is off, a file on disk is ignored. +def firebaseEnabledRaw = project.findProperty('firebaseEnabled') +def firebaseEnabled = true +if (firebaseEnabledRaw != null) { + def normalized = firebaseEnabledRaw.toString().trim().toLowerCase() + if (normalized == 'false' || normalized == '0' || normalized == 'no') { + firebaseEnabled = false } -} catch(Exception e) { - logger.info("google-services plugin not applied: ${e.message}") +} + +if (!firebaseEnabled) { + logger.lifecycle('Firebase disabled (-PfirebaseEnabled=false): google-services plugin not applied') +} else { + def servicesJSON = file('google-services.json') + def servicesBody = servicesJSON.isFile() ? servicesJSON.getText('UTF-8') : '' + if (!servicesBody.trim() || !servicesBody.contains('mobilesdk_app_id')) { + throw new GradleException( + 'Firebase is enabled, but android/app/google-services.json is missing, empty, or has no mobilesdk_app_id.\n' + + 'Download it from the Firebase console (Project settings → Your apps → Android app)\n' + + 'and place it at android/app/google-services.json.\n' + + 'To build without Firebase (F-Droid / FOSS), pass -PfirebaseEnabled=false.' + ) + } + apply plugin: 'com.google.gms.google-services' + logger.lifecycle('Firebase enabled: google-services plugin applied') } diff --git a/android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java b/android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java index 963322ec..b70c05b4 100644 --- a/android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java +++ b/android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java @@ -15,8 +15,8 @@ public class NotificationInspectorPlugin extends Plugin { } /** - * Whether this build carries a Firebase config, i.e. was built with - * -PfirebaseEnabled. Without it, PushNotifications.register() throws on the + * Whether this build carries a Firebase config. Normal builds do. + * -PfirebaseEnabled=false omits it. Without it, PushNotifications.register() throws on the * Capacitor plugin thread and kills the app, so JS must check this first. * The google-services Gradle plugin generates the google_app_id resource, * which is what Firebase's own auto-initialization looks for. diff --git a/android/gradle.properties b/android/gradle.properties index 98bddd32..bbda0b2e 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -22,3 +22,8 @@ org.gradle.jvmargs=-Xmx1536m --add-opens=jdk.compiler/com.sun.tools.javac.api=AL # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true android.suppressUnsupportedCompileSdk=36 + +# Firebase/FCM is on for every Gradle build (Android Studio, ./gradlew, npm scripts). +# Opt out with -PfirebaseEnabled=false. android/app/google-services.json does not +# toggle this; a missing file fails the build while Firebase is enabled. +firebaseEnabled=true diff --git a/doc/android-firebase-gms.md b/doc/android-firebase-gms.md index 2fde5452..b6de160d 100644 --- a/doc/android-firebase-gms.md +++ b/doc/android-firebase-gms.md @@ -2,49 +2,45 @@ ## Overview -The app is designed to work on Android devices with and without Google Play Services (GMS). Firebase/FCM push notifications are an opt-in feature at build time; all other functionality works on GMS-less devices (F-Droid, LineageOS without OpenGApps, etc.). +The app is designed to work on Android devices with and without Google Play Services (GMS). Firebase/FCM is **on by default** for local, test, and production builds. GMS-less devices still run the app; FCM degrades when Play Services is absent. F-Droid and other builds that must omit Firebase opt out explicitly. -## How the opt-in guard works +## How the switch works -`android/app/build.gradle` applies the `com.google.gms.google-services` Gradle plugin only when **both** conditions are true: +`android/app/build.gradle` applies `com.google.gms.google-services` unless `firebaseEnabled` is `false`. `android/gradle.properties` sets `firebaseEnabled=true`, so Android Studio, `./gradlew`, and `scripts/build-android.sh` all pick it up. `-PfirebaseEnabled` and `-PfirebaseEnabled=true` stay on. The config file does not decide the switch. -1. `android/google-services.json` is present on disk -2. The Gradle property `firebaseEnabled` is explicitly passed +When Firebase is on, `android/app/google-services.json` must exist, be non-empty, and contain `mobilesdk_app_id`. Otherwise configuration fails with an error that names that path. When Firebase is off, the file is ignored even if it is on disk. -```groovy -if (servicesJSON.exists() && servicesJSON.text && project.hasProperty('firebaseEnabled')) { - apply plugin: 'com.google.gms.google-services' -} -``` +`scripts/build-android.sh` forwards an opt-out: `FIREBASE_ENABLED=false` adds `-PfirebaseEnabled=false` to its Gradle commands. npm scripts do not pass `-PfirebaseEnabled`; they rely on the default. -This means the file can live on disk for development purposes without accidentally activating Firebase. +iOS uses the same default. `ios/App/scripts/bundle-firebase-config.sh` copies `ios/App/App/GoogleService-Info.plist` into the app and the share extension. `scripts/build-ios.sh` runs it with `--check` before a device build, and both Xcode targets run it as a build phase. `FIREBASE_ENABLED=false` skips the plist. A missing or empty plist fails the build when Firebase is on. ## Build commands | Target | Command | Firebase | |---|---|---| -| APK / sideload / Zapstore | `./gradlew assembleRelease` | off | -| Aurora / Play Store without FCM | `./gradlew bundleRelease` | off | -| Play Store with FCM push notifications | `./gradlew bundleRelease -PfirebaseEnabled` | on | -| F-Droid | `./gradlew assembleRelease` | off (required) | +| Local / test / production APK or AAB | `./gradlew assembleRelease` or `bundleRelease` | on | +| Play Store with FCM | `./gradlew bundleRelease` | on | +| iOS (Xcode or `scripts/build-ios.sh`) | default | on | +| F-Droid / FOSS Android | `./gradlew assembleRelease -PfirebaseEnabled=false` | off | +| iOS without Firebase | `FIREBASE_ENABLED=false` | off | ## Behavior on non-GMS devices -When built with `-PfirebaseEnabled`, Firebase SDKs check for GMS availability at startup and degrade gracefully if it is absent: +When Firebase is enabled (the default), Firebase SDKs check for GMS availability at startup and degrade gracefully if it is absent: - Firebase initializes but detects no GMS - FCM skips token registration silently (no token, no notifications) - The app continues to work normally -This means a single Play Store AAB (`bundleRelease -PfirebaseEnabled`) covers both GMS and non-GMS users. GMS users get push notifications; non-GMS users get a fully functional app without them. +This means a single Play Store AAB (`bundleRelease`, Firebase on by default) covers both GMS and non-GMS users. GMS users get push notifications; non-GMS users get a fully functional app without them. -**Aurora Store** pulls the exact APK from Play Store servers, so Aurora users get whichever variant was uploaded. The Play Store AAB built with `-PfirebaseEnabled` is correct for Aurora. +**Aurora Store** pulls the exact APK from Play Store servers, so Aurora users get whichever variant was uploaded. The default Play Store AAB (Firebase on) is the one to upload. -**F-Droid** is stricter: their build policy rejects any APK with GMS dependencies at the binary level, even with graceful degradation. F-Droid submission would require a separate `assembleRelease` build (no flag) and a dedicated F-Droid listing. +**F-Droid** is stricter: their build policy rejects any APK with GMS dependencies at the binary level, even with graceful degradation. F-Droid submission would require a separate `assembleRelease -PfirebaseEnabled=false` build and a dedicated F-Droid listing. ## Push registration in builds without Firebase -A build without `-PfirebaseEnabled` contains the `@capacitor/push-notifications` plugin, but no Firebase config. In that build, `PushNotifications.register()` throws `Default FirebaseApp is not initialized` on the Capacitor plugin thread, which kills the app; a JS `try/catch` cannot intercept it. +A build with `-PfirebaseEnabled=false` contains the `@capacitor/push-notifications` plugin, but no Firebase config. In that build, `PushNotifications.register()` throws `Default FirebaseApp is not initialized` on the Capacitor plugin thread, which kills the app; a JS `try/catch` cannot intercept it. The JS therefore asks native code first. `NotificationInspector.isFirebaseConfigured()` (`android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java`) reports whether the `google_app_id` string resource exists; the google-services Gradle plugin generates that resource only when Firebase is enabled, and it is the same signal Firebase's auto-initialization uses. `firebaseMessagingClient.ts` skips `register()` on Android when it is absent (or when the check fails), logging `No Firebase config in this Android build; skipping push registration`. Listener setup and the notification permission request run either way, since local daily notifications need that permission. @@ -56,7 +52,8 @@ Any code that calls `PushNotifications.register()` or other `FirebaseMessaging` - Gitignored (`android/.gitignore` line 80) — never commit it - Contains Firebase project credentials (project number, app ID, API key) -- Safe to leave on disk; has no effect unless `-PfirebaseEnabled` is passed +- Required for the default (Firebase on) build. Place it at `android/app/google-services.json` +- Ignored when you pass `-PfirebaseEnabled=false`; leaving it on disk does not re-enable Firebase - Obtain from the Firebase console: Project Settings → Your apps → Android app → Download `google-services.json` ## Known GMS dependency: MLKit barcode scanner diff --git a/doc/local-android-testing-ngrok.md b/doc/local-android-testing-ngrok.md index f3e032c9..294bb560 100644 --- a/doc/local-android-testing-ngrok.md +++ b/doc/local-android-testing-ngrok.md @@ -240,7 +240,7 @@ Complete this section once before your first physical-device push test. If Fireb 2. The repo gitignores this file — each developer keeps a local copy; never commit it. -3. Rebuild so Gradle applies the Google Services plugin (`android/app/build.gradle` applies `com.google.gms.google-services` when the file exists). If the file is missing, the build logs: *google-services.json not found, google-services plugin not applied. Push Notifications won't work*. +3. Rebuild. Firebase is on by default, so Gradle applies the Google Services plugin. If `android/app/google-services.json` is missing or empty, the build stops and names that path. Opt out only with `-PfirebaseEnabled=false` or `FIREBASE_ENABLED=false`. 4. Sync Capacitor if you changed native config: @@ -928,11 +928,11 @@ Compare with panel **Backend Status** and Event Log error text. ### Gradle: Push Notifications won't work -**Symptoms:** Build log: `google-services.json not found, google-services plugin not applied`. +**Symptoms:** Build fails: `android/app/google-services.json is missing, empty, or has no mobilesdk_app_id`. **Likely causes:** Missing `android/app/google-services.json`. -**Verification:** File exists on disk; rebuild shows Google Services plugin applied. +**Verification:** File exists on disk and the build log says `Firebase enabled: google-services plugin applied`. **Fixes:** Download from Firebase Console (package `app.timesafari.app`); place in `android/app/`; `npx cap sync android` and rebuild. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index cccb52cf..5c74a99c 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -19,8 +19,6 @@ C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; }; C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; }; - C8E73DD12FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8E73DD02FC6E5DC0057F59A /* GoogleService-Info.plist */; }; - C8E73DD22FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8E73DD02FC6E5DC0057F59A /* GoogleService-Info.plist */; }; E9F1A0022EE05A8B00737D01 /* NotificationInspectorPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F1A0012EE05A8B00737D01 /* NotificationInspectorPlugin.swift */; }; /* End PBXBuildFile section */ @@ -185,6 +183,7 @@ 504EC3001FED79650016851F /* Sources */, 504EC3011FED79650016851F /* Frameworks */, 504EC3021FED79650016851F /* Resources */, + F1REBA5E000000000000A001 /* Bundle Firebase Config */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, 96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */, C86585E02ED456DE00824752 /* Embed Foundation Extensions */, @@ -207,6 +206,7 @@ C86585D12ED456DE00824752 /* Sources */, C86585D22ED456DE00824752 /* Frameworks */, C86585D32ED456DE00824752 /* Resources */, + F1REBA5E000000000000A002 /* Bundle Firebase Config */, ); buildRules = ( ); @@ -270,7 +270,6 @@ 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, 504EC30D1FED79650016851F /* Main.storyboard in Resources */, 2FAD9763203C412B000D30F8 /* config.xml in Resources */, - C8E73DD12FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -278,7 +277,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C8E73DD22FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -342,6 +340,48 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + F1REBA5E000000000000A001 /* Bundle Firebase Config */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + enableUserScriptSandboxing = 0; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Bundle Firebase Config"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}/scripts/bundle-firebase-config.sh\"\n"; + showEnvVarsInLog = 0; + }; + F1REBA5E000000000000A002 /* Bundle Firebase Config */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + enableUserScriptSandboxing = 0; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Bundle Firebase Config"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}/scripts/bundle-firebase-config.sh\"\n"; + showEnvVarsInLog = 0; + }; 96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/App/scripts/bundle-firebase-config.sh b/ios/App/scripts/bundle-firebase-config.sh new file mode 100755 index 00000000..078d8d5c --- /dev/null +++ b/ios/App/scripts/bundle-firebase-config.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Bundle GoogleService-Info.plist when Firebase is enabled (the default). +# Opt out with FIREBASE_ENABLED=false. The plist does not decide the switch: +# a missing file fails an enabled build, and an opt-out build does not bundle +# the plist even when the file is on disk. +# +# Xcode run-script phases invoke this with no arguments. build-ios.sh may pass +# --check to validate before a long build without copying into a product. +set -eu + +check_only=0 +if [ "${1:-}" = "--check" ]; then + check_only=1 +fi + +if [ -z "${SRCROOT:-}" ]; then + script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) + SRCROOT=$(CDPATH= cd -- "$script_dir/.." && pwd) +fi + +normalized=$(printf '%s' "${FIREBASE_ENABLED:-true}" | tr '[:upper:]' '[:lower:]') +plist="${SRCROOT}/App/GoogleService-Info.plist" + +firebase_off=0 +case "$normalized" in + false|0|no) firebase_off=1 ;; +esac + +if [ "$firebase_off" -eq 1 ]; then + if [ "$check_only" -eq 0 ]; then + dest_dir="${TARGET_BUILD_DIR:-}/${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}" + if [ -n "${TARGET_BUILD_DIR:-}" ] && [ -n "${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}" ]; then + rm -f "${dest_dir}/GoogleService-Info.plist" + fi + fi + echo "note: Firebase disabled (FIREBASE_ENABLED=false); GoogleService-Info.plist will not be bundled." + exit 0 +fi + +if [ ! -s "$plist" ] || ! grep -q "GOOGLE_APP_ID" "$plist"; then + echo "error: Firebase is enabled, but ios/App/App/GoogleService-Info.plist is missing, empty, or has no GOOGLE_APP_ID." >&2 + echo "error: Download it from the Firebase console (Project settings → Your apps → Apple apps)" >&2 + echo "error: and place it at ios/App/App/GoogleService-Info.plist." >&2 + echo "error: To build without Firebase, set FIREBASE_ENABLED=false." >&2 + exit 1 +fi + +if [ "$check_only" -eq 1 ]; then + echo "note: Firebase enabled; ios/App/App/GoogleService-Info.plist is present." + exit 0 +fi + +if [ -z "${TARGET_BUILD_DIR:-}" ] || [ -z "${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}" ]; then + echo "error: Bundle Firebase Config must run as an Xcode build phase (TARGET_BUILD_DIR is unset)." >&2 + exit 1 +fi + +dest_dir="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +mkdir -p "$dest_dir" +cp "$plist" "${dest_dir}/GoogleService-Info.plist" +echo "note: Firebase enabled; bundled GoogleService-Info.plist." diff --git a/scripts/build-android.sh b/scripts/build-android.sh index 129e94c8..c953ab4c 100755 --- a/scripts/build-android.sh +++ b/scripts/build-android.sh @@ -23,6 +23,7 @@ # --assets Generate assets only # --deploy Deploy APK to connected device # --uninstall Uninstall app from connected device +# FIREBASE_ENABLED=false Opt out of Firebase (-PfirebaseEnabled=false) # -h, --help Show this help message # -v, --verbose Enable verbose logging # @@ -439,6 +440,9 @@ print_android_usage() { echo " --uninstall Uninstall app from connected device" echo " --api-ip Custom IP address for claim API (defaults to 10.0.2.2)" echo "" + echo "Firebase is on by default. Opt out with FIREBASE_ENABLED=false" + echo "(passes -PfirebaseEnabled=false to Gradle)." + echo "" echo "Common Options:" echo " -h, --help Show this help message" echo " -v, --verbose Enable verbose logging" @@ -460,6 +464,17 @@ print_android_usage() { # Parse command line arguments parse_android_args "$@" +# Firebase is on unless FIREBASE_ENABLED is false, 0, or no. Gradle also +# defaults firebaseEnabled=true; this only forwards an explicit opt-out. +gradle_firebase_arg="" +firebase_enabled_normalized=$(printf '%s' "${FIREBASE_ENABLED:-true}" | tr '[:upper:]' '[:lower:]') +case "$firebase_enabled_normalized" in + false|0|no) + gradle_firebase_arg="-PfirebaseEnabled=false" + log_info "Firebase disabled for this Android build (FIREBASE_ENABLED=${FIREBASE_ENABLED})" + ;; +esac + # Print build header print_header "TimeSafari Android Build Process" @@ -528,7 +543,7 @@ if [ "$CLEAN_ONLY" = true ]; then log_info "Clean-only mode: cleaning build artifacts" safe_execute "Cleaning Android app" "npm run clean:android" || exit 1 safe_execute "Cleaning dist directory" "clean_build_artifacts dist" || exit 1 - safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4 + safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean ${gradle_firebase_arg} && cd .." || exit 4 log_success "Clean completed successfully!" exit 0 fi @@ -561,7 +576,7 @@ if [ "$DEPLOY_APP" = true ]; then fi # Build APK - safe_execute "Building APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5 + safe_execute "Building APK" "cd android && ./gradlew assembleDebug ${gradle_firebase_arg} && cd .." || exit 5 # Install APK on device safe_execute "Installing APK on device" "adb install -r android/app/build/outputs/apk/debug/app-debug.apk" || exit 6 @@ -628,26 +643,26 @@ fi safe_execute "Generating assets" "npx capacitor-assets generate --android --assetPath resources" || exit 7 # Step 8: Clean Gradle build -safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4 +safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean ${gradle_firebase_arg} && cd .." || exit 4 # Step 9: Build based on type if [ "$BUILD_TYPE" = "debug" ]; then - safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5 + safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug ${gradle_firebase_arg} && cd .." || exit 5 elif [ "$BUILD_TYPE" = "release" ]; then - safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5 + safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease ${gradle_firebase_arg} && cd .." || exit 5 fi # Step 10: Build APK/AAB if requested if [ "$BUILD_APK" = true ]; then if [ "$BUILD_TYPE" = "debug" ]; then - safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5 + safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug ${gradle_firebase_arg} && cd .." || exit 5 else - safe_execute "Building release APK" "cd android && ./gradlew assembleRelease && cd .." || exit 5 + safe_execute "Building release APK" "cd android && ./gradlew assembleRelease ${gradle_firebase_arg} && cd .." || exit 5 fi fi if [ "$BUILD_AAB" = true ]; then - safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5 + safe_execute "Building AAB" "cd android && ./gradlew bundleRelease ${gradle_firebase_arg} && cd .." || exit 5 if [ "$BUILD_MODE" = "production" ]; then safe_execute "Checking production AAB" "./scripts/check-prod-bundle.sh android/app/build/outputs/bundle/release/app-release.aab" || exit 10 fi diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh index b432c124..100efb81 100755 --- a/scripts/build-ios.sh +++ b/scripts/build-ios.sh @@ -44,6 +44,8 @@ print_ios_usage() { echo " --auto-run Auto-run app after build" echo " --api-ip Custom IP address for claim API (uses Capacitor default)" echo "" + echo "Firebase is on by default. Opt out with FIREBASE_ENABLED=false." + echo "" echo "Common Options:" echo " -h, --help Show this help message" echo " -v, --verbose Enable verbose logging" @@ -537,6 +539,13 @@ if [ "$ASSETS_ONLY" = true ]; then exit 0 fi +# Firebase is on unless FIREBASE_ENABLED=false. Fail before the web build and +# xcodebuild when the plist is missing. Xcode runs the same script as a build +# phase so GUI builds get the same check and bundle the plist. +if ! ios/App/scripts/bundle-firebase-config.sh --check; then + exit 1 +fi + # Handle deploy-app mode if [ "$DEPLOY_APP" = true ]; then deploy_ios_app diff --git a/src/plugins/NotificationInspectorPlugin.ts b/src/plugins/NotificationInspectorPlugin.ts index 27d6885b..bd426f75 100644 --- a/src/plugins/NotificationInspectorPlugin.ts +++ b/src/plugins/NotificationInspectorPlugin.ts @@ -11,7 +11,7 @@ export type PendingNotificationInfo = { export interface NotificationInspectorPlugin { getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>; - /** Android only: whether the build has a Firebase config (-PfirebaseEnabled). */ + /** Android only: whether this build included Firebase (off only with -PfirebaseEnabled=false). */ isFirebaseConfigured(): Promise<{ configured: boolean }>; } diff --git a/src/services/notifications/firebaseMessagingClient.ts b/src/services/notifications/firebaseMessagingClient.ts index 5558c08e..cc7f475c 100644 --- a/src/services/notifications/firebaseMessagingClient.ts +++ b/src/services/notifications/firebaseMessagingClient.ts @@ -69,9 +69,10 @@ async function registerRetrievedToken( } /** - * Whether PushNotifications.register() is safe to call. Android builds without - * -PfirebaseEnabled (FOSS/F-Droid/APK) have no Firebase config, and register() - * there throws natively, crashing the app before any JS catch can run. + * Whether PushNotifications.register() is safe to call. Android builds with + * Firebase disabled (-PfirebaseEnabled=false, FOSS/F-Droid) have no Firebase + * config, and register() there throws natively, crashing the app before any + * JS catch can run. */ async function canRegisterNativePush(): Promise { if (Capacitor.getPlatform() !== "android") { @@ -101,7 +102,7 @@ export async function reregisterFcmTokenNow(): Promise { if (!(await canRegisterNativePush())) { throw new Error( - "Push is unavailable: this Android build has no Firebase config (built without -PfirebaseEnabled)", + "Push is unavailable: this Android build has no Firebase config (built with -PfirebaseEnabled=false)", ); }