diff --git a/android/src/main/java/com/getcapacitor/community/fcm/FCMPlugin.java b/android/src/main/java/com/getcapacitor/community/fcm/FCMPlugin.java index 94e2c77..1dfa020 100644 --- a/android/src/main/java/com/getcapacitor/community/fcm/FCMPlugin.java +++ b/android/src/main/java/com/getcapacitor/community/fcm/FCMPlugin.java @@ -6,6 +6,8 @@ import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.CapacitorPlugin; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; import com.google.firebase.installations.FirebaseInstallations; import com.google.firebase.messaging.FirebaseMessaging; @@ -20,6 +22,13 @@ public class FCMPlugin extends Plugin { public static final String TAG = "FirebaseMessaging"; + private FirebaseApp secondaryApp; + private FirebaseMessaging secondaryMessaging; + + private FirebaseMessaging getFirebaseMessaging() { + return secondaryApp != null ? secondaryMessaging : FirebaseMessaging.getInstance(); + } + @PluginMethod public void subscribeTo(final PluginCall call) { final String topicName = call.getString("topic"); @@ -61,7 +70,7 @@ public void deleteInstance(final PluginCall call) { @PluginMethod public void getToken(final PluginCall call) { - FirebaseMessaging.getInstance() + getFirebaseMessaging() .getToken() .addOnCompleteListener(getActivity(), tokenResult -> { if (!tokenResult.isSuccessful()) { @@ -75,15 +84,15 @@ public void getToken(final PluginCall call) { call.resolve(data); }); - FirebaseMessaging.getInstance().getToken().addOnFailureListener(e -> call.reject("Failed to get FCM registration token", e)); + getFirebaseMessaging().getToken().addOnFailureListener(e -> call.reject("Failed to get FCM registration token", e)); } @PluginMethod public void refreshToken(final PluginCall call) { - FirebaseMessaging.getInstance() + getFirebaseMessaging() .deleteToken() .addOnCompleteListener(result -> { - FirebaseMessaging.getInstance() + getFirebaseMessaging() .getToken() .addOnCompleteListener(getActivity(), tokenResult -> { JSObject data = new JSObject(); @@ -98,15 +107,45 @@ public void refreshToken(final PluginCall call) { @PluginMethod public void setAutoInit(final PluginCall call) { final boolean enabled = call.getBoolean("enabled", false); - FirebaseMessaging.getInstance().setAutoInitEnabled(enabled); + getFirebaseMessaging().setAutoInitEnabled(enabled); call.resolve(); } @PluginMethod public void isAutoInitEnabled(final PluginCall call) { - final boolean enabled = FirebaseMessaging.getInstance().isAutoInitEnabled(); + final boolean enabled = getFirebaseMessaging().isAutoInitEnabled(); JSObject data = new JSObject(); data.put("enabled", enabled); call.resolve(data); } + + @PluginMethod + public void setFirebaseOptions(final PluginCall call) { + String applicationId = call.getString("applicationId"); + String apiKey = call.getString("apiKey"); + String projectId = call.getString("projectId"); + + // gcmSenderId is accepted for interface parity with iOS but omitted from + // FirebaseOptions.Builder — setGcmSenderId() was removed in Firebase Android SDK 29+. + FirebaseOptions options = new FirebaseOptions.Builder() + .setApplicationId(applicationId) + .setApiKey(apiKey) + .setProjectId(projectId) + .build(); + + // Use a named secondary app ("FCM") so we don't conflict with or + // re-initialize the default app that may already exist from google-services.json. + FirebaseApp existing = null; + for (FirebaseApp app : FirebaseApp.getApps(getContext())) { + if ("FCM".equals(app.getName())) { + existing = app; + break; + } + } + secondaryApp = existing != null + ? existing + : FirebaseApp.initializeApp(getContext(), options, "FCM"); + secondaryMessaging = secondaryApp.get(FirebaseMessaging.class); + call.resolve(); + } } diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index b48c713..f9beb34 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -17,7 +17,11 @@ public class FCMPlugin: CAPPlugin, MessagingDelegate { var fcmToken: String? override public func load() { - if FirebaseApp.app() == nil { + // Only auto-configure from plist when a GoogleService-Info.plist is present. + // Apps using setFirebaseOptions() for dynamic/multi-tenant config must NOT + // ship GoogleService-Info.plist — Firebase will be configured at runtime instead. + if FirebaseApp.app() == nil, + Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist") != nil { FirebaseApp.configure() } Messaging.messaging().delegate = self @@ -132,4 +136,28 @@ public class FCMPlugin: CAPPlugin, MessagingDelegate { @objc public func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { self.fcmToken = fcmToken } + + @objc func setFirebaseOptions(_ call: CAPPluginCall) { + let applicationId = call.getString("applicationId") ?? "" + let gcmSenderId = call.getString("gcmSenderId") ?? "" + let apiKey = call.getString("apiKey") ?? "" + let projectId = call.getString("projectId") ?? "" + + let options = FirebaseOptions(googleAppID: applicationId, gcmSenderID: gcmSenderId) + options.apiKey = apiKey + options.projectID = projectId + + // Configure the default Firebase app with runtime options. + // This path is taken when no GoogleService-Info.plist is present + // (load() skips auto-configuration in that case). + // Re-initialization after FirebaseApp.configure() is not supported by the + // Firebase iOS SDK; apps that need to switch tenants must restart. + guard FirebaseApp.app() == nil else { + call.reject("Firebase is already configured. setFirebaseOptions() must be called before any Firebase initialization. Do not ship GoogleService-Info.plist when using dynamic configuration.") + return + } + FirebaseApp.configure(options: options) + Messaging.messaging().delegate = self + call.resolve() + } } diff --git a/src/definitions.ts b/src/definitions.ts index 90db19e..d7b5ca6 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -1,3 +1,23 @@ +export interface FirebaseOptions { + /** + * The Firebase app ID (mobilesdk_app_id on Android, GOOGLE_APP_ID on iOS). + */ + applicationId: string; + /** + * The GCM sender ID (project_number on Android / iOS). + * Required on iOS; ignored on Android (deprecated in Firebase Android SDK 29+). + */ + gcmSenderId: string; + /** + * The Firebase API key. + */ + apiKey: string; + /** + * The Firebase project ID. + */ + projectId: string; +} + export interface FCMPlugin { /** * Subscribe to fcm topic @@ -54,4 +74,23 @@ export interface FCMPlugin { * Retrieve the auto initialization status. */ isAutoInitEnabled(): Promise<{ enabled: boolean }>; + + /** + * Initialize Firebase with runtime credentials instead of relying on a + * static google-services.json / GoogleService-Info.plist. Intended for + * multi-tenant apps where the correct Firebase project is only known after + * the user authenticates. + * + * Must be called before PushNotifications.register() / getToken(). + * + * Platform notes: + * - Android: creates a named secondary FirebaseApp ("FCM") so it does not + * conflict with any default app already initialized from google-services.json. + * getToken() / refreshToken() are automatically routed through it. + * - iOS: configures the default FirebaseApp when no GoogleService-Info.plist + * is present. Do not ship GoogleService-Info.plist when using this method + * on iOS. Re-initialization after first configure is not supported (Firebase + * SDK limitation). + */ + setFirebaseOptions(options: FirebaseOptions): Promise; } diff --git a/src/web.ts b/src/web.ts index 607fb34..14d4854 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,6 +1,6 @@ import { WebPlugin } from '@capacitor/core'; -import type { FCMPlugin } from './definitions'; +import type { FCMPlugin, FirebaseOptions } from './definitions'; export class FCMWeb extends WebPlugin implements FCMPlugin { constructor() { @@ -34,6 +34,10 @@ export class FCMWeb extends WebPlugin implements FCMPlugin { refreshToken(): Promise<{ token: string }> { throw this.unimplemented('Not implemented on web.'); } + + setFirebaseOptions(_options: FirebaseOptions): Promise { + throw this.unimplemented('Not implemented on web.'); + } } const FCM = new FCMWeb();