-
Notifications
You must be signed in to change notification settings - Fork 378
fix: [SDK-4946] prefer google-services.json over the shared FCM project #2725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package com.onesignal.notifications.internal.registration.impl | ||
|
|
||
| import com.google.android.gms.tasks.Task | ||
| import com.google.android.gms.tasks.Tasks | ||
| import java.lang.reflect.InvocationTargetException | ||
| import java.util.concurrent.ExecutionException | ||
|
|
||
| /** | ||
| * Retrieves an FCM registration token, falling back to Firebase Installation ID registration | ||
| * when the host app has opted into it. | ||
| * | ||
| * Opting in (via `firebase_messaging_installation_id_enabled`) disables the legacy token API | ||
| * for the whole process, not just the [com.google.firebase.FirebaseApp] that opted in. In that | ||
| * case the only usable token is a Firebase Installation ID issued by a real Firebase project — | ||
| * which means the host app's `google-services.json`, not OneSignal's shared project. | ||
| */ | ||
| internal object FCMTokenProvider { | ||
| /** | ||
| * The Firebase Installation ID registration that replaces the legacy token API, along with the | ||
| * sender id of the Firebase project it would register against. | ||
| */ | ||
| class InstallationIdRegistration( | ||
| val senderId: String?, | ||
| val register: () -> Task<*>, | ||
| val installationId: () -> Task<String>, | ||
| ) | ||
|
|
||
| fun getToken( | ||
| senderId: String, | ||
| installationIdEnabled: () -> String, | ||
| legacyToken: () -> Task<String>, | ||
| installationIdRegistration: () -> InstallationIdRegistration?, | ||
| ): String { | ||
| return try { | ||
| await(legacyToken()) | ||
| } catch (e: IllegalStateException) { | ||
| if (!isLegacyTokenApiDisabled(e)) throw e | ||
|
|
||
| registerInstallationId(senderId, installationIdEnabled(), installationIdRegistration()) | ||
| } | ||
| } | ||
|
|
||
| private fun registerInstallationId( | ||
| senderId: String, | ||
| installationIdEnabled: String, | ||
| registration: InstallationIdRegistration?, | ||
| ): String { | ||
| val optedIn = "firebase_messaging_installation_id_enabled=$installationIdEnabled" | ||
|
|
||
| if (registration == null) { | ||
| throw IllegalStateException( | ||
| "Firebase Installation ID registration is enabled ($optedIn) but this app has no " + | ||
| "default FirebaseApp to register with. Add your Firebase configuration " + | ||
| "(google-services.json) and apply the google-services Gradle plugin, or set " + | ||
| "firebase_messaging_installation_id_enabled to false in your manifest to keep " + | ||
| "using the legacy FCM token API.", | ||
| ) | ||
| } | ||
|
|
||
| if (registration.senderId != senderId) { | ||
| throw IllegalStateException( | ||
| "Firebase Installation ID registration is enabled ($optedIn) but the default " + | ||
| "FirebaseApp uses sender id ${registration.senderId}, while OneSignal is " + | ||
| "configured with sender id $senderId. Point google-services.json and the " + | ||
| "OneSignal dashboard at the same Firebase project, or set " + | ||
| "firebase_messaging_installation_id_enabled to false in your manifest to keep " + | ||
| "using the legacy FCM token API.", | ||
| ) | ||
| } | ||
|
|
||
| await(registration.register()) | ||
| return await(registration.installationId()) | ||
| } | ||
|
|
||
| /** | ||
| * Calls `register()` reflectively. [com.google.firebase.messaging.FirebaseMessaging.register] | ||
| * was added in firebase-messaging 25.1.0. This module compiles against the preferred 24.0.0, | ||
| * but the non-strict Gradle constraint lets apps select newer versions through conflict | ||
| * resolution. | ||
| */ | ||
| fun invokeRegister(target: Any): Task<*> { | ||
| val register = | ||
| try { | ||
| target.javaClass.getMethod("register") | ||
| } catch (e: NoSuchMethodException) { | ||
| throw IllegalStateException( | ||
| "Firebase Installation ID registration is enabled but " + | ||
| "FirebaseMessaging.register() was not found. It requires firebase-messaging " + | ||
| "25.1.0 or newer, and has to survive minification, so check that OneSignal's " + | ||
| "consumer ProGuard rules are applied.", | ||
| e, | ||
| ) | ||
| } | ||
|
|
||
| return try { | ||
| register.invoke(target) as Task<*> | ||
| } catch (e: InvocationTargetException) { | ||
| throw e.targetException ?: e | ||
| } | ||
| } | ||
|
|
||
| private fun isLegacyTokenApiDisabled(exception: IllegalStateException): Boolean { | ||
| val message = exception.message ?: return false | ||
| return message.contains("API disabled") && message.contains("register()") | ||
| } | ||
|
|
||
| private fun <T> await(task: Task<T>): T { | ||
| try { | ||
| return Tasks.await(task) | ||
| } catch (e: ExecutionException) { | ||
| throw task.exception ?: e | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package com.onesignal.notifications.internal.registration.impl | ||
|
|
||
| /** | ||
| * Client-side Firebase project credentials used to register for FCM. | ||
| * | ||
| * Google requires the sender id, project id, application id, and api key to all belong to the | ||
| * same Firebase project. Mixing a customer's sender id with OneSignal's shared project is what | ||
| * the legacy path did, and Play Services rejects that for Firebase Installation ID registration. | ||
| */ | ||
| internal data class FcmProjectCredentials( | ||
| val senderId: String, | ||
| val projectId: String, | ||
| val applicationId: String, | ||
| val apiKey: String, | ||
| ) { | ||
| val isComplete: Boolean | ||
| get() = | ||
| senderId.isNotBlank() && | ||
| projectId.isNotBlank() && | ||
| applicationId.isNotBlank() && | ||
| apiKey.isNotBlank() | ||
| } | ||
|
|
||
| internal data class FcmFirebaseConfig( | ||
| val credentials: FcmProjectCredentials, | ||
| val source: Source, | ||
| val reuseDefaultApp: Boolean, | ||
| ) { | ||
| enum class Source { | ||
| /** | ||
| * Host app's default [com.google.firebase.FirebaseApp], initialized from | ||
| * `google-services.json` (the google-services Gradle plugin compiles that file into | ||
| * string resources; [com.google.firebase.FirebaseApp.initializeApp] reads them). | ||
| */ | ||
| GOOGLE_SERVICES, | ||
|
|
||
| /** Complete `fcm` object from `android_params.js`. */ | ||
| BACKEND, | ||
|
|
||
| /** | ||
| * OneSignal's shared public Firebase project. Kept as a last-resort fallback for apps | ||
| * that never added `google-services.json`. Does not work with Installation ID registration. | ||
| */ | ||
| SHARED_DEFAULT, | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Picks a single consistent Firebase project for FCM registration. | ||
| * | ||
| * Preference order: | ||
| * 1. The host app's default Firebase app, when its sender id matches the OneSignal dashboard. | ||
| * 2. Backend-provided FCM params (project id / app id / api key) with the dashboard sender id. | ||
| * 3. OneSignal's shared public project, still pairing the dashboard sender id (legacy). | ||
| */ | ||
| internal object FcmFirebaseConfigResolver { | ||
| fun resolve( | ||
| dashboardSenderId: String, | ||
| defaultApp: FcmProjectCredentials?, | ||
| backend: FcmProjectCredentials?, | ||
| sharedDefault: FcmProjectCredentials, | ||
| ): FcmFirebaseConfig { | ||
| val matchingDefaultApp = | ||
| defaultApp?.takeIf { it.isComplete && it.senderId == dashboardSenderId } | ||
| val completeBackend = backend?.takeIf { it.isComplete } | ||
| return when { | ||
| matchingDefaultApp != null -> | ||
| FcmFirebaseConfig( | ||
| credentials = matchingDefaultApp, | ||
| source = FcmFirebaseConfig.Source.GOOGLE_SERVICES, | ||
| reuseDefaultApp = true, | ||
| ) | ||
| completeBackend != null -> | ||
| FcmFirebaseConfig( | ||
| credentials = completeBackend, | ||
| source = FcmFirebaseConfig.Source.BACKEND, | ||
| reuseDefaultApp = false, | ||
|
Comment on lines
+63
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning (3/3 models):
Treat backend params that match the shared defaults — or whose |
||
| ) | ||
| else -> | ||
| FcmFirebaseConfig( | ||
| credentials = sharedDefault, | ||
| source = FcmFirebaseConfig.Source.SHARED_DEFAULT, | ||
| reuseDefaultApp = false, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical (3/3 models):
register()isTask<Void>; the value uploaded is a separateFirebaseInstallations.idhop, not the identifier FCM just registered.On Play Services below the V1 threshold (
GMS_VERSION_Y2026W12/261200000in firebase-messaging 25.1),FirebaseMessaging.register()silently takes the legacygetTokenpath.getToken()still throws “API disabled” from the manifest flag alone, so this branch runs,register()succeeds with a normal FCM token, and the SDK reports an FID that was never registered as a send target. Result:SUBSCRIBEDwith an identifier that cannot receive pushes, with no log distinguishing it.Do not synthesize the token independently of what FCM produced. Gate the FID path on Play Services support, or use the identifier FCM actually registered (the value
blockingRegister/onRegistereddelivers).