Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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");
Expand Down Expand Up @@ -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()) {
Expand All @@ -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();
Expand All @@ -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();
}
}
30 changes: 29 additions & 1 deletion ios/Plugin/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
39 changes: 39 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<void>;
}
6 changes: 5 additions & 1 deletion src/web.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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<void> {
throw this.unimplemented('Not implemented on web.');
}
}

const FCM = new FCMWeb();
Expand Down