diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index bdd3e55c..28240b47 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -54,5 +54,9 @@ { "pkg": "SharedImage", "classpath": "app.timesafari.sharedimage.SharedImagePlugin" + }, + { + "pkg": "NotificationInspector", + "classpath": "app.timesafari.notifications.NotificationInspectorPlugin" } ] 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 82c6d0ae..963322ec 100644 --- a/android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java +++ b/android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java @@ -1,5 +1,6 @@ package app.timesafari.notifications; +import com.getcapacitor.JSObject; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; @@ -12,5 +13,20 @@ public class NotificationInspectorPlugin extends Plugin { call.unimplemented( "Pending notification inspection is currently implemented on iOS only"); } -} + /** + * Whether this build carries a Firebase config, i.e. was built with + * -PfirebaseEnabled. 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. + */ + @PluginMethod + public void isFirebaseConfigured(PluginCall call) { + int resId = getContext().getResources().getIdentifier( + "google_app_id", "string", getContext().getPackageName()); + JSObject ret = new JSObject(); + ret.put("configured", resId != 0); + call.resolve(ret); + } +} diff --git a/scripts/restore-local-plugins.js b/scripts/restore-local-plugins.js index 3b947d6a..1ac686c5 100755 --- a/scripts/restore-local-plugins.js +++ b/scripts/restore-local-plugins.js @@ -2,7 +2,7 @@ /** * Restore Local Capacitor Plugins * - * This script ensures that local custom plugins (SafeArea and SharedImage) + * This script ensures that local custom plugins (SafeArea, SharedImage, NotificationInspector) * are present in capacitor.plugins.json after `npx cap sync` runs. * * The capacitor.plugins.json file is auto-generated by Capacitor and gets @@ -28,6 +28,10 @@ const LOCAL_PLUGINS = [ { pkg: 'SharedImage', classpath: 'app.timesafari.sharedimage.SharedImagePlugin' + }, + { + pkg: 'NotificationInspector', + classpath: 'app.timesafari.notifications.NotificationInspectorPlugin' } ]; diff --git a/src/plugins/NotificationInspectorPlugin.ts b/src/plugins/NotificationInspectorPlugin.ts index 8acecc37..27d6885b 100644 --- a/src/plugins/NotificationInspectorPlugin.ts +++ b/src/plugins/NotificationInspectorPlugin.ts @@ -11,6 +11,8 @@ export type PendingNotificationInfo = { export interface NotificationInspectorPlugin { getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>; + /** Android only: whether the build has a Firebase config (-PfirebaseEnabled). */ + isFirebaseConfigured(): Promise<{ configured: boolean }>; } export const NotificationInspector = diff --git a/src/services/notifications/firebaseMessagingClient.ts b/src/services/notifications/firebaseMessagingClient.ts index a618c610..5558c08e 100644 --- a/src/services/notifications/firebaseMessagingClient.ts +++ b/src/services/notifications/firebaseMessagingClient.ts @@ -21,6 +21,7 @@ import { onMessage, } from "firebase/messaging"; import { logger } from "@/utils/logger"; +import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin"; import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService"; import { getNotificationApiHeaders } from "./notificationApiAuth"; import { deferFcmRegistration } from "./notificationAuthLifecycle"; @@ -67,6 +68,24 @@ async function registerRetrievedToken( lastRegisteredFcmToken = trimmed; } +/** + * 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. + */ +async function canRegisterNativePush(): Promise { + if (Capacitor.getPlatform() !== "android") { + return true; + } + try { + const { configured } = await NotificationInspector.isFirebaseConfigured(); + return configured; + } catch (err) { + logger.warn(`${LOG} Could not check Firebase config; skipping push`, err); + return false; + } +} + /** Most recent FCM token from native/web push registration (for debug UI). */ export function getLastKnownFcmToken(): string | null { return lastSeenFcmToken ?? lastRegisteredFcmToken; @@ -80,6 +99,12 @@ export async function reregisterFcmTokenNow(): Promise { throw new Error("FCM registration is only available on native platforms"); } + if (!(await canRegisterNativePush())) { + throw new Error( + "Push is unavailable: this Android build has no Firebase config (built without -PfirebaseEnabled)", + ); + } + lastRegisteredFcmToken = null; const cached = lastSeenFcmToken?.trim(); @@ -290,6 +315,13 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise { return; } + if (!(await canRegisterNativePush())) { + logger.info( + `${LOG} No Firebase config in this Android build; skipping push registration`, + ); + return; + } + await PushNotifications.register(); if (app) {