fix(android): skip push registration in builds without Firebase, which crashed the app at launch

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-09-27 21:06:35 -06:00
co-authored by Claude Opus 5.5
parent 28ee81b9d3
commit 2fd5549098
5 changed files with 60 additions and 2 deletions
@@ -54,5 +54,9 @@
{ {
"pkg": "SharedImage", "pkg": "SharedImage",
"classpath": "app.timesafari.sharedimage.SharedImagePlugin" "classpath": "app.timesafari.sharedimage.SharedImagePlugin"
},
{
"pkg": "NotificationInspector",
"classpath": "app.timesafari.notifications.NotificationInspectorPlugin"
} }
] ]
@@ -1,5 +1,6 @@
package app.timesafari.notifications; package app.timesafari.notifications;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin; import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall; import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod; import com.getcapacitor.PluginMethod;
@@ -12,5 +13,20 @@ public class NotificationInspectorPlugin extends Plugin {
call.unimplemented( call.unimplemented(
"Pending notification inspection is currently implemented on iOS only"); "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);
}
}
+5 -1
View File
@@ -2,7 +2,7 @@
/** /**
* Restore Local Capacitor Plugins * 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. * are present in capacitor.plugins.json after `npx cap sync` runs.
* *
* The capacitor.plugins.json file is auto-generated by Capacitor and gets * The capacitor.plugins.json file is auto-generated by Capacitor and gets
@@ -28,6 +28,10 @@ const LOCAL_PLUGINS = [
{ {
pkg: 'SharedImage', pkg: 'SharedImage',
classpath: 'app.timesafari.sharedimage.SharedImagePlugin' classpath: 'app.timesafari.sharedimage.SharedImagePlugin'
},
{
pkg: 'NotificationInspector',
classpath: 'app.timesafari.notifications.NotificationInspectorPlugin'
} }
]; ];
@@ -11,6 +11,8 @@ export type PendingNotificationInfo = {
export interface NotificationInspectorPlugin { export interface NotificationInspectorPlugin {
getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>; getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>;
/** Android only: whether the build has a Firebase config (-PfirebaseEnabled). */
isFirebaseConfigured(): Promise<{ configured: boolean }>;
} }
export const NotificationInspector = export const NotificationInspector =
@@ -21,6 +21,7 @@ import {
onMessage, onMessage,
} from "firebase/messaging"; } from "firebase/messaging";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService"; import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
import { getNotificationApiHeaders } from "./notificationApiAuth"; import { getNotificationApiHeaders } from "./notificationApiAuth";
import { deferFcmRegistration } from "./notificationAuthLifecycle"; import { deferFcmRegistration } from "./notificationAuthLifecycle";
@@ -67,6 +68,24 @@ async function registerRetrievedToken(
lastRegisteredFcmToken = trimmed; 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<boolean> {
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). */ /** Most recent FCM token from native/web push registration (for debug UI). */
export function getLastKnownFcmToken(): string | null { export function getLastKnownFcmToken(): string | null {
return lastSeenFcmToken ?? lastRegisteredFcmToken; return lastSeenFcmToken ?? lastRegisteredFcmToken;
@@ -80,6 +99,12 @@ export async function reregisterFcmTokenNow(): Promise<string> {
throw new Error("FCM registration is only available on native platforms"); 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; lastRegisteredFcmToken = null;
const cached = lastSeenFcmToken?.trim(); const cached = lastSeenFcmToken?.trim();
@@ -290,6 +315,13 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
return; return;
} }
if (!(await canRegisterNativePush())) {
logger.info(
`${LOG} No Firebase config in this Android build; skipping push registration`,
);
return;
}
await PushNotifications.register(); await PushNotifications.register();
if (app) { if (app) {