From 8c0b02d9a21d314ed4ba88b4be60984f6f6e8436 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 17:52:40 -0700 Subject: [PATCH 01/10] feat(fcm): register before dashboard provides sender ID --- .../impl/PushRegistratorAbstractGoogle.kt | 7 +- .../registration/impl/PushRegistratorFCM.kt | 65 +++++++++----- .../impl/PushRegistratorFCMTests.kt | 87 ++++++++++++++++++- 3 files changed, 131 insertions(+), 28 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt index ac24ca3558..7454f448ea 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt @@ -51,6 +51,8 @@ internal abstract class PushRegistratorAbstractGoogle( @Throws(ExecutionException::class, InterruptedException::class, IOException::class) abstract suspend fun getToken(senderId: String): String + protected open fun resolveSenderId(configuredSenderId: String?): String? = configuredSenderId + override suspend fun registerForPush(): IPushRegistrator.RegisterResult { if (!_configModelStore.model.isInitializedWithRemote) { return IPushRegistrator.RegisterResult(null, SubscriptionStatus.FIREBASE_FCM_INIT_ERROR) @@ -61,7 +63,8 @@ internal abstract class PushRegistratorAbstractGoogle( return IPushRegistrator.RegisterResult(null, SubscriptionStatus.MISSING_FIREBASE_FCM_LIBRARY) } - return if (!isValidProjectNumber(_configModelStore.model.googleProjectNumber)) { + val senderId = resolveSenderId(_configModelStore.model.googleProjectNumber) + return if (!isValidProjectNumber(senderId)) { Logging.warn( "Missing Google Project number!\nPlease enter a Google Project number / Sender ID on under App Settings > Android > Configuration on the OneSignal dashboard.", ) @@ -70,7 +73,7 @@ internal abstract class PushRegistratorAbstractGoogle( SubscriptionStatus.INVALID_FCM_SENDER_ID, ) } else { - internalRegisterForPush(_configModelStore.model.googleProjectNumber!!) + internalRegisterForPush(senderId!!) } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index 61b03010d5..67adc770dd 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -54,16 +54,28 @@ internal class PushRegistratorFCM( @Throws(ExecutionException::class, InterruptedException::class) override suspend fun getToken(senderId: String): String { + val defaultApp = defaultFirebaseApp() return FCMTokenProvider.getToken( senderId = senderId, installationIdEnabled = ::installationIdEnabled, - legacyToken = { getLegacyToken(senderId) }, + legacyToken = { getLegacyToken(senderId, defaultApp) }, installationIdApiAvailable = { FCMTokenProvider.hasRegisterMethod(FirebaseMessaging::class.java) }, - installationIdRegistration = ::defaultAppRegistration, + installationIdRegistration = { defaultApp?.let(::installationIdRegistration) }, ) } - private fun getLegacyToken(senderId: String): Task { + override fun resolveSenderId(configuredSenderId: String?): String? = + configuredSenderId ?: defaultFirebaseApp()?.let(::defaultSenderId) + + private fun getLegacyToken( + senderId: String, + defaultApp: FirebaseApp?, + ): Task { + val defaultSenderId = defaultApp?.let(::defaultSenderId) + if (defaultSenderId != null) { + FCMTokenProvider.validateSenderId(senderId, defaultSenderId) + } + val app = initFirebaseApp(senderId) // We use the named app's FirebaseMessaging instance instead of FirebaseMessaging.getInstance() // as the latter uses the default Firebase app. We need to use a custom Firebase app as @@ -82,24 +94,25 @@ internal class PushRegistratorFCM( // Installation ID registration is rejected unless the sender id, app id, and api key all belong // to the same Firebase project. Our own FirebaseApp pairs the app's sender id with OneSignal's // shared project credentials, so only the host app's default FirebaseApp can be used for it. - private fun defaultAppRegistration(): FCMTokenProvider.InstallationIdRegistration? { - val defaultApp = - FirebaseApp - .getApps(_applicationService.appContext) - .firstOrNull { it.name == FirebaseApp.DEFAULT_APP_NAME } ?: return null - val defaultSenderId = - FCMTokenProvider.defaultSenderId( - defaultApp.options.gcmSenderId, - defaultApp.options.applicationId, - ) - + private fun installationIdRegistration(defaultApp: FirebaseApp): FCMTokenProvider.InstallationIdRegistration { return FCMTokenProvider.InstallationIdRegistration( - senderId = defaultSenderId, + senderId = defaultSenderId(defaultApp), register = { FCMTokenProvider.invokeRegister(defaultApp.get(FirebaseMessaging::class.java)) }, installationId = { FirebaseInstallations.getInstance(defaultApp).id }, ) } + private fun defaultFirebaseApp(): FirebaseApp? = + FirebaseApp + .getApps(_applicationService.appContext) + .firstOrNull { it.name == FirebaseApp.DEFAULT_APP_NAME } + + private fun defaultSenderId(defaultApp: FirebaseApp): String? = + FCMTokenProvider.defaultSenderId( + defaultApp.options.gcmSenderId, + defaultApp.options.applicationId, + ) + private fun initFirebaseApp(senderId: String): FirebaseApp { firebaseApp?.let { return it } val firebaseOptions = @@ -178,20 +191,24 @@ internal object FCMTokenProvider { ) } - 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 both 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.", - ) - } + validateSenderId(senderId, registration.senderId) await(registration.register()) return await(registration.installationId()) } + fun validateSenderId( + senderId: String, + defaultSenderId: String?, + ) { + if (defaultSenderId == senderId) return + + throw IllegalStateException( + "The default FirebaseApp uses sender id $defaultSenderId, while OneSignal is " + + "configured with sender id $senderId. Point both at the same Firebase project.", + ) + } + /** * Calls register() reflectively. FirebaseMessaging.register was added in firebase-messaging * 25.1.0. This module compiles against the preferred 24.0.0, but the non-strict Gradle diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt index d5206fa762..9d09e216d4 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt @@ -12,7 +12,10 @@ import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.messaging.FirebaseMessaging import com.onesignal.common.AndroidUtils import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.device.IDeviceService import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.subscriptions.SubscriptionStatus import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe @@ -47,6 +50,8 @@ private fun defaultApp( private fun registrator( legacyToken: Task, installedApps: List = emptyList(), + configModelStore: ConfigModelStore = MockHelper.configModelStore(), + deviceService: IDeviceService = mockk(relaxed = true), ): PushRegistratorFCM { val messaging = mockk() every { messaging.token } returns legacyToken @@ -62,10 +67,10 @@ private fun registrator( every { applicationService.appContext } returns ApplicationProvider.getApplicationContext() return PushRegistratorFCM( - MockHelper.configModelStore(), + configModelStore, applicationService, mockk(relaxed = true), - mockk(relaxed = true), + deviceService, ) } @@ -83,6 +88,31 @@ class PushRegistratorFCMTests : FunSpec({ token shouldBe "fcm-token" } + test("registers a legacy FCM token using the locally derived sender id before the dashboard provides one") { + val app = defaultApp(null) + val configModelStore = + MockHelper.configModelStore { + it.isInitializedWithRemote = true + it.googleProjectNumber = null + } + val deviceService = mockk() + every { deviceService.hasFCMLibrary } returns true + every { deviceService.isGMSInstalledAndEnabled } returns true + val registrator = + registrator( + legacyToken = Tasks.forResult("fcm-token"), + installedApps = listOf(app), + configModelStore = configModelStore, + deviceService = deviceService, + ) + + val result = withContext(Dispatchers.IO) { registrator.registerForPush() } + + result.id shouldBe "fcm-token" + result.status shouldBe SubscriptionStatus.SUBSCRIBED + verify(exactly = 1) { FirebaseApp.initializeApp(any(), any(), any()) } + } + test("registers the installation id through the matching default FirebaseApp") { val metaData = Bundle().apply { putBoolean("firebase_messaging_installation_id_enabled", true) } mockkObject(AndroidUtils) @@ -110,6 +140,41 @@ class PushRegistratorFCMTests : FunSpec({ verify(exactly = 0) { FirebaseApp.initializeApp(any(), any(), any()) } } + test("registers an installation id before the dashboard has a sender id") { + val metaData = Bundle().apply { putBoolean("firebase_messaging_installation_id_enabled", true) } + mockkObject(AndroidUtils) + every { AndroidUtils.getManifestMetaBundle(any()) } returns metaData + val messaging = mockk() + val app = defaultApp(null, messaging) + val installations = mockk() + every { installations.id } returns Tasks.forResult("installation-id") + mockkStatic(FirebaseInstallations::class) + every { FirebaseInstallations.getInstance(app) } returns installations + mockkObject(FCMTokenProvider) + every { FCMTokenProvider.hasRegisterMethod(FirebaseMessaging::class.java) } returns true + every { FCMTokenProvider.invokeRegister(messaging) } returns Tasks.forResult(null) + val configModelStore = + MockHelper.configModelStore { + it.isInitializedWithRemote = true + it.googleProjectNumber = null + } + val deviceService = mockk() + every { deviceService.hasFCMLibrary } returns true + every { deviceService.isGMSInstalledAndEnabled } returns true + val registrator = + registrator( + legacyToken = Tasks.forResult("unused-fcm-token"), + installedApps = listOf(app), + configModelStore = configModelStore, + deviceService = deviceService, + ) + + val result = withContext(Dispatchers.IO) { registrator.registerForPush() } + + result.id shouldBe "installation-id" + result.status shouldBe SubscriptionStatus.SUBSCRIBED + } + test("explains the problem when the app has no default FirebaseApp to register with") { val registrator = registrator(legacyToken = Tasks.forException(disabledLegacyApi)) @@ -136,4 +201,22 @@ class PushRegistratorFCMTests : FunSpec({ thrown.message!! shouldContain "sender id 999999999999" } + + test("does not request a legacy token from a default FirebaseApp with a different sender id") { + val messaging = mockk() + every { messaging.token } returns Tasks.forResult("wrong-project-token") + val registrator = + registrator( + legacyToken = Tasks.forResult("unused-fcm-token"), + installedApps = listOf(defaultApp("999999999999", messaging)), + ) + + val thrown = + withContext(Dispatchers.IO) { + shouldThrow { registrator.getToken(SENDER_ID) } + } + + thrown.message!! shouldContain "sender id 999999999999" + verify(exactly = 0) { messaging.token } + } }) From cc261327b88b96e47145aaccec3f819572387573 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 18:06:10 -0700 Subject: [PATCH 02/10] build(demo): make google-services optional --- .gitignore | 1 + examples/demo/app/build.gradle.kts | 5 +++++ examples/demo/build.gradle.kts | 1 + 3 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index b2f46c5ab1..55284aa52b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ local.properties OneSignalSDK/local.properties examples/demo/local.properties +google-services.json # macOS .DS_Store diff --git a/examples/demo/app/build.gradle.kts b/examples/demo/app/build.gradle.kts index 25aafbaacb..51620700d4 100644 --- a/examples/demo/app/build.gradle.kts +++ b/examples/demo/app/build.gradle.kts @@ -32,6 +32,11 @@ if (taskRequests.contains("huawei")) { apply(plugin = "com.huawei.agconnect") } +// Firebase configuration is local so the demo must remain buildable without it. +if (file("google-services.json").exists()) { + apply(plugin = "com.google.gms.google-services") +} + // OneSignal SDK version - can be overridden via gradle property SDK_VERSION val sdkVersion: String = rootProject.findProperty("SDK_VERSION") as? String ?: "5.6.1" diff --git a/examples/demo/build.gradle.kts b/examples/demo/build.gradle.kts index a12a9da2f0..f45de3d727 100644 --- a/examples/demo/build.gradle.kts +++ b/examples/demo/build.gradle.kts @@ -11,6 +11,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:8.8.2") + classpath("com.google.gms:google-services:4.3.10") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") classpath("com.huawei.agconnect:agcp:1.9.1.304") } From d71828d118d6e41de9d1aee235b4a82e6e6ac5fb Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 18:17:59 -0700 Subject: [PATCH 03/10] build: bump google-services to 4.4.2 --- OneSignalSDK/build.gradle | 2 +- examples/demo/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle index 6a29fc4016..f35d3cee7d 100644 --- a/OneSignalSDK/build.gradle +++ b/OneSignalSDK/build.gradle @@ -10,7 +10,7 @@ buildscript { ] androidGradlePluginVersion = '8.8.2' detektVersion = '1.21.0' - googleServicesGradlePluginVersion = '4.3.10' + googleServicesGradlePluginVersion = '4.4.2' huaweiAgconnectVersion = '1.9.1.304' huaweiHMSPushVersion = '6.3.0.304' huaweiHMSLocationVersion = '4.0.0.300' diff --git a/examples/demo/build.gradle.kts b/examples/demo/build.gradle.kts index f45de3d727..b093064104 100644 --- a/examples/demo/build.gradle.kts +++ b/examples/demo/build.gradle.kts @@ -11,7 +11,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:8.8.2") - classpath("com.google.gms:google-services:4.3.10") + classpath("com.google.gms:google-services:4.4.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") classpath("com.huawei.agconnect:agcp:1.9.1.304") } From 73b05afe56f5eee6d9496b83f23e5daa87bcea48 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 4 Sep 2026 13:12:28 -0700 Subject: [PATCH 04/10] fix: [SDK-5170] handle legacy sender changes Co-authored-by: Cursor --- .../registration/impl/PushRegistratorFCM.kt | 25 ++++++------- .../impl/PushRegistratorFCMTests.kt | 36 +++++++++++++++---- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index 67adc770dd..e4d689e452 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -40,6 +40,7 @@ internal class PushRegistratorFCM( private val apiKey: String private var firebaseApp: FirebaseApp? = null + private var firebaseAppSenderId: String? = null override val providerName: String get() = "FCM" @@ -58,7 +59,7 @@ internal class PushRegistratorFCM( return FCMTokenProvider.getToken( senderId = senderId, installationIdEnabled = ::installationIdEnabled, - legacyToken = { getLegacyToken(senderId, defaultApp) }, + legacyToken = { getLegacyToken(senderId) }, installationIdApiAvailable = { FCMTokenProvider.hasRegisterMethod(FirebaseMessaging::class.java) }, installationIdRegistration = { defaultApp?.let(::installationIdRegistration) }, ) @@ -67,15 +68,7 @@ internal class PushRegistratorFCM( override fun resolveSenderId(configuredSenderId: String?): String? = configuredSenderId ?: defaultFirebaseApp()?.let(::defaultSenderId) - private fun getLegacyToken( - senderId: String, - defaultApp: FirebaseApp?, - ): Task { - val defaultSenderId = defaultApp?.let(::defaultSenderId) - if (defaultSenderId != null) { - FCMTokenProvider.validateSenderId(senderId, defaultSenderId) - } - + private fun getLegacyToken(senderId: String): Task { val app = initFirebaseApp(senderId) // We use the named app's FirebaseMessaging instance instead of FirebaseMessaging.getInstance() // as the latter uses the default Firebase app. We need to use a custom Firebase app as @@ -114,7 +107,12 @@ internal class PushRegistratorFCM( ) private fun initFirebaseApp(senderId: String): FirebaseApp { - firebaseApp?.let { return it } + firebaseApp?.let { + if (firebaseAppSenderId == senderId) return it + it.delete() + firebaseApp = null + firebaseAppSenderId = null + } val firebaseOptions = FirebaseOptions .Builder() @@ -124,7 +122,10 @@ internal class PushRegistratorFCM( .setProjectId(projectId) .build() return FirebaseApp.initializeApp(_applicationService.appContext, firebaseOptions, FCM_APP_NAME) - .also { firebaseApp = it } + .also { + firebaseApp = it + firebaseAppSenderId = senderId + } } } diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt index 9d09e216d4..996674e6d8 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt @@ -88,6 +88,30 @@ class PushRegistratorFCMTests : FunSpec({ token shouldBe "fcm-token" } + test("recreates OneSignal's FirebaseApp when the sender id changes") { + val firstMessaging = mockk() + every { firstMessaging.token } returns Tasks.forResult("first-token") + val firstApp = mockk(relaxed = true) + every { firstApp.get(FirebaseMessaging::class.java) } returns firstMessaging + val secondMessaging = mockk() + every { secondMessaging.token } returns Tasks.forResult("second-token") + val secondApp = mockk(relaxed = true) + every { secondApp.get(FirebaseMessaging::class.java) } returns secondMessaging + val initializedOptions = mutableListOf() + val registrator = registrator(legacyToken = Tasks.forResult("unused-token")) + every { + FirebaseApp.initializeApp(any(), capture(initializedOptions), any()) + } returnsMany listOf(firstApp, secondApp) + + val firstToken = withContext(Dispatchers.IO) { registrator.getToken(SENDER_ID) } + val secondToken = withContext(Dispatchers.IO) { registrator.getToken("999999999999") } + + firstToken shouldBe "first-token" + secondToken shouldBe "second-token" + initializedOptions.map { it.gcmSenderId } shouldBe listOf(SENDER_ID, "999999999999") + verify(exactly = 1) { firstApp.delete() } + } + test("registers a legacy FCM token using the locally derived sender id before the dashboard provides one") { val app = defaultApp(null) val configModelStore = @@ -202,21 +226,19 @@ class PushRegistratorFCMTests : FunSpec({ thrown.message!! shouldContain "sender id 999999999999" } - test("does not request a legacy token from a default FirebaseApp with a different sender id") { + test("uses OneSignal's FirebaseApp for a legacy token when the default app has a different sender id") { val messaging = mockk() every { messaging.token } returns Tasks.forResult("wrong-project-token") val registrator = registrator( - legacyToken = Tasks.forResult("unused-fcm-token"), + legacyToken = Tasks.forResult("fcm-token"), installedApps = listOf(defaultApp("999999999999", messaging)), ) - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { registrator.getToken(SENDER_ID) } - } + val token = withContext(Dispatchers.IO) { registrator.getToken(SENDER_ID) } - thrown.message!! shouldContain "sender id 999999999999" + token shouldBe "fcm-token" verify(exactly = 0) { messaging.token } + verify(exactly = 1) { FirebaseApp.initializeApp(any(), any(), any()) } } }) From ad5b01b8f6e58790a6c9c38231724865aca85236 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 8 Sep 2026 13:54:35 -0700 Subject: [PATCH 05/10] fix(notifications): serialize push token retrievals --- .../listeners/DeviceRegistrationListener.kt | 18 ++++--- .../DeviceRegistrationListenerTests.kt | 53 +++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListener.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListener.kt index d3af8c822b..b7eeee68d1 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListener.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListener.kt @@ -16,6 +16,8 @@ import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionModel import com.onesignal.user.internal.subscriptions.SubscriptionStatus import com.onesignal.user.subscriptions.ISubscription +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * The device registration listener will subscribe to events and at the appropriate time will @@ -33,6 +35,8 @@ internal class DeviceRegistrationListener( ISingletonModelStoreChangeHandler, IPermissionObserver, ISubscriptionChangedHandler { + private val pushTokenMutex = Mutex() + override fun start() { _configModelStore.subscribe(this) _notificationsManager.addPermissionObserver(this) @@ -94,12 +98,14 @@ internal class DeviceRegistrationListener( val pushSubscription = _subscriptionManager.subscriptions.push suspendifyOnIO { - val pushTokenAndStatus = _pushTokenManager.retrievePushToken() - val permission = _notificationsManager.permission - _subscriptionManager.addOrUpdatePushSubscriptionToken( - pushTokenAndStatus.token, - if (permission) pushTokenAndStatus.status else SubscriptionStatus.NO_PERMISSION, - ) + pushTokenMutex.withLock { + val pushTokenAndStatus = _pushTokenManager.retrievePushToken() + val permission = _notificationsManager.permission + _subscriptionManager.addOrUpdatePushSubscriptionToken( + pushTokenAndStatus.token, + if (permission) pushTokenAndStatus.status else SubscriptionStatus.NO_PERMISSION, + ) + } } } diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListenerTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListenerTests.kt index 3009177772..70f5f5a21b 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListenerTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/listeners/DeviceRegistrationListenerTests.kt @@ -1,5 +1,8 @@ package com.onesignal.notifications.internal.listeners +import com.onesignal.common.modeling.ModelChangeTags +import com.onesignal.common.threading.suspendifyOnIO +import com.onesignal.core.internal.config.ConfigModel import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging @@ -14,11 +17,16 @@ import com.onesignal.user.internal.subscriptions.SubscriptionModel import com.onesignal.user.internal.subscriptions.SubscriptionStatus import com.onesignal.user.internal.subscriptions.SubscriptionType import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async private const val NEW_TOKEN = "new-token" @@ -270,4 +278,49 @@ class DeviceRegistrationListenerTests : FunSpec({ ) } } + + test("serializes overlapping startup and hydration token retrievals") { + val harness = + Harness( + permission = true, + pushModel = uninitializedPushModel(), + ) + val blocks = mutableListOf Unit>() + every { suspendifyOnIO(any Unit>()) } answers { + blocks += firstArg Unit>() + } + val firstStarted = CompletableDeferred() + val finishFirst = CompletableDeferred() + var requestCount = 0 + coEvery { harness.pushTokenManager.retrievePushToken() } coAnswers { + if (requestCount++ == 0) { + firstStarted.complete(Unit) + finishFirst.await() + PushTokenResponse("startup-token", SubscriptionStatus.SUBSCRIBED) + } else { + PushTokenResponse("hydrated-token", SubscriptionStatus.SUBSCRIBED) + } + } + val updates = mutableListOf() + every { + harness.subscriptionManager.addOrUpdatePushSubscriptionToken(any(), any()) + } answers { + updates += firstArg() + } + + harness.listener.start() + harness.listener.onModelReplaced(mockk(relaxed = true), ModelChangeTags.HYDRATE) + + val startup = async(Dispatchers.Default) { blocks[0]() } + firstStarted.await() + val hydration = + async(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + blocks[1]() + } + finishFirst.complete(Unit) + startup.await() + hydration.await() + + updates shouldBe listOf("startup-token", "hydrated-token") + } }) From 834cd266c141eae51903e8ac1241cd53abb7312a Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 8 Sep 2026 18:41:12 -0700 Subject: [PATCH 06/10] refactor(notifications): simplify null-safe sender ID check --- .../impl/PushRegistratorAbstractGoogle.kt | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt index 7454f448ea..6ff846944a 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt @@ -64,7 +64,7 @@ internal abstract class PushRegistratorAbstractGoogle( } val senderId = resolveSenderId(_configModelStore.model.googleProjectNumber) - return if (!isValidProjectNumber(senderId)) { + return if (senderId == null || !isValidProjectNumber(senderId)) { Logging.warn( "Missing Google Project number!\nPlease enter a Google Project number / Sender ID on under App Settings > Android > Configuration on the OneSignal dashboard.", ) @@ -73,7 +73,7 @@ internal abstract class PushRegistratorAbstractGoogle( SubscriptionStatus.INVALID_FCM_SENDER_ID, ) } else { - internalRegisterForPush(senderId!!) + internalRegisterForPush(senderId) } } @@ -183,21 +183,7 @@ internal abstract class PushRegistratorAbstractGoogle( } } - private fun isValidProjectNumber(senderId: String?): Boolean { - val isProjectNumberValidFormat: Boolean = - try { - senderId!!.toFloat() - true - } catch (t: Throwable) { - false - } - - if (!isProjectNumberValidFormat) { - return false - } - - return true - } + private fun isValidProjectNumber(senderId: String): Boolean = senderId.toFloatOrNull() != null companion object { private const val REGISTRATION_RETRY_COUNT = 5 From eca519a3a08ffce91671d6b48990da4afa8a6190 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 8 Sep 2026 18:45:17 -0700 Subject: [PATCH 07/10] refactor(notifications): clarify host Firebase naming Co-authored-by: Cursor --- .../registration/impl/PushRegistratorFCM.kt | 32 +++++++++---------- .../impl/FCMTokenProviderTests.kt | 10 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index e4d689e452..bd8d69a656 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -55,18 +55,18 @@ internal class PushRegistratorFCM( @Throws(ExecutionException::class, InterruptedException::class) override suspend fun getToken(senderId: String): String { - val defaultApp = defaultFirebaseApp() + val hostApp = hostFirebaseApp() return FCMTokenProvider.getToken( senderId = senderId, installationIdEnabled = ::installationIdEnabled, legacyToken = { getLegacyToken(senderId) }, installationIdApiAvailable = { FCMTokenProvider.hasRegisterMethod(FirebaseMessaging::class.java) }, - installationIdRegistration = { defaultApp?.let(::installationIdRegistration) }, + installationIdRegistration = { hostApp?.let(::installationIdRegistration) }, ) } override fun resolveSenderId(configuredSenderId: String?): String? = - configuredSenderId ?: defaultFirebaseApp()?.let(::defaultSenderId) + configuredSenderId ?: hostFirebaseApp()?.let(::hostFirebaseSenderId) private fun getLegacyToken(senderId: String): Task { val app = initFirebaseApp(senderId) @@ -87,23 +87,23 @@ internal class PushRegistratorFCM( // Installation ID registration is rejected unless the sender id, app id, and api key all belong // to the same Firebase project. Our own FirebaseApp pairs the app's sender id with OneSignal's // shared project credentials, so only the host app's default FirebaseApp can be used for it. - private fun installationIdRegistration(defaultApp: FirebaseApp): FCMTokenProvider.InstallationIdRegistration { + private fun installationIdRegistration(hostApp: FirebaseApp): FCMTokenProvider.InstallationIdRegistration { return FCMTokenProvider.InstallationIdRegistration( - senderId = defaultSenderId(defaultApp), - register = { FCMTokenProvider.invokeRegister(defaultApp.get(FirebaseMessaging::class.java)) }, - installationId = { FirebaseInstallations.getInstance(defaultApp).id }, + senderId = hostFirebaseSenderId(hostApp), + register = { FCMTokenProvider.invokeRegister(hostApp.get(FirebaseMessaging::class.java)) }, + installationId = { FirebaseInstallations.getInstance(hostApp).id }, ) } - private fun defaultFirebaseApp(): FirebaseApp? = + private fun hostFirebaseApp(): FirebaseApp? = FirebaseApp .getApps(_applicationService.appContext) .firstOrNull { it.name == FirebaseApp.DEFAULT_APP_NAME } - private fun defaultSenderId(defaultApp: FirebaseApp): String? = - FCMTokenProvider.defaultSenderId( - defaultApp.options.gcmSenderId, - defaultApp.options.applicationId, + private fun hostFirebaseSenderId(hostApp: FirebaseApp): String? = + FCMTokenProvider.firebaseAppSenderId( + hostApp.options.gcmSenderId, + hostApp.options.applicationId, ) private fun initFirebaseApp(senderId: String): FirebaseApp { @@ -130,7 +130,7 @@ internal class PushRegistratorFCM( } internal object FCMTokenProvider { - fun defaultSenderId( + fun firebaseAppSenderId( senderId: String?, applicationId: String, ): String? = @@ -200,12 +200,12 @@ internal object FCMTokenProvider { fun validateSenderId( senderId: String, - defaultSenderId: String?, + firebaseAppSenderId: String?, ) { - if (defaultSenderId == senderId) return + if (firebaseAppSenderId == senderId) return throw IllegalStateException( - "The default FirebaseApp uses sender id $defaultSenderId, while OneSignal is " + + "The default FirebaseApp uses sender id $firebaseAppSenderId, while OneSignal is " + "configured with sender id $senderId. Point both at the same Firebase project.", ) } diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt index 74ad9c4684..ded081b0c4 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt @@ -38,11 +38,11 @@ private class WithoutRegister class FCMTokenProviderTests : FunSpec({ val disabledLegacyApi = IllegalStateException("API disabled. Please use {@link #register()} instead.") - test("derives the default sender id the same way as Firebase Messaging") { - FCMTokenProvider.defaultSenderId(SENDER_ID, "ignored") shouldBe SENDER_ID - FCMTokenProvider.defaultSenderId(null, "1:$SENDER_ID:android:abc") shouldBe SENDER_ID - FCMTokenProvider.defaultSenderId(null, "legacy-application-id") shouldBe "legacy-application-id" - FCMTokenProvider.defaultSenderId(null, "1::android:abc") shouldBe null + test("derives the Firebase app sender id the same way as Firebase Messaging") { + FCMTokenProvider.firebaseAppSenderId(SENDER_ID, "ignored") shouldBe SENDER_ID + FCMTokenProvider.firebaseAppSenderId(null, "1:$SENDER_ID:android:abc") shouldBe SENDER_ID + FCMTokenProvider.firebaseAppSenderId(null, "legacy-application-id") shouldBe "legacy-application-id" + FCMTokenProvider.firebaseAppSenderId(null, "1::android:abc") shouldBe null } test("returns the legacy FCM token when installation id registration is unavailable") { From 5e5cdafdc9cf5d40d985b009ef7db5b8edb6442d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 8 Sep 2026 18:47:34 -0700 Subject: [PATCH 08/10] refactor(notifications): clarify resolved sender naming Co-authored-by: Cursor --- .../internal/registration/impl/PushRegistratorFCM.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index bd8d69a656..e93c31a85f 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -106,9 +106,9 @@ internal class PushRegistratorFCM( hostApp.options.applicationId, ) - private fun initFirebaseApp(senderId: String): FirebaseApp { + private fun initFirebaseApp(resolvedSenderId: String): FirebaseApp { firebaseApp?.let { - if (firebaseAppSenderId == senderId) return it + if (firebaseAppSenderId == resolvedSenderId) return it it.delete() firebaseApp = null firebaseAppSenderId = null @@ -116,7 +116,7 @@ internal class PushRegistratorFCM( val firebaseOptions = FirebaseOptions .Builder() - .setGcmSenderId(senderId) + .setGcmSenderId(resolvedSenderId) .setApplicationId(appId) .setApiKey(apiKey) .setProjectId(projectId) @@ -124,7 +124,7 @@ internal class PushRegistratorFCM( return FirebaseApp.initializeApp(_applicationService.appContext, firebaseOptions, FCM_APP_NAME) .also { firebaseApp = it - firebaseAppSenderId = senderId + firebaseAppSenderId = resolvedSenderId } } } From ae24e1c4a7e9a3010547dc323a69af21132233e5 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 8 Sep 2026 18:49:00 -0700 Subject: [PATCH 09/10] docs(notifications): explain resolved sender source Co-authored-by: Cursor --- .../internal/registration/impl/PushRegistratorFCM.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index e93c31a85f..697011537c 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -106,6 +106,10 @@ internal class PushRegistratorFCM( hostApp.options.applicationId, ) + /** + * @param resolvedSenderId sender ID from the dashboard configuration, or the host Firebase app + * sender ID when the dashboard has not provided one. + */ private fun initFirebaseApp(resolvedSenderId: String): FirebaseApp { firebaseApp?.let { if (firebaseAppSenderId == resolvedSenderId) return it From 6a099c446af8a278e5270aa23a481e3bbf829061 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 8 Sep 2026 18:53:01 -0700 Subject: [PATCH 10/10] fix(notifications): report FID sender mismatch Co-authored-by: Cursor --- .../internal/pushtoken/PushTokenManager.kt | 22 +++++++------------ .../impl/PushRegistratorAbstractGoogle.kt | 10 +++++++++ .../registration/impl/PushRegistratorFCM.kt | 4 +++- .../pushtoken/PushTokenManagerTests.kt | 18 +++++++++++++++ .../impl/PushRegistratorFCMTests.kt | 20 ++++++++++++----- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt index 5813d156bb..d916e79161 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt @@ -33,19 +33,10 @@ internal class PushTokenManager( if (registerResult.status.value == SubscriptionStatus.SUBSCRIBED.value) { pushTokenStatus = registerResult.status } else if (registerResult.status.value < SubscriptionStatus.SUBSCRIBED.value) { - // Only allow errored statuses if we have never gotten a token. This ensures the - // device will not later be marked unsubscribed due to any inconsistencies returned - // by Google Play services. Also do not override a config error status if we got a - // runtime error - if (pushToken == null && - ( - pushTokenStatus == SubscriptionStatus.NO_PERMISSION || - pushStatusRuntimeError(pushTokenStatus) - ) - ) { + if (shouldUpdateErrorStatus(registerResult.status)) { pushTokenStatus = registerResult.status } - } else if (pushStatusRuntimeError(pushTokenStatus)) { + } else if (pushTokenStatus.isRetryableTokenError) { pushTokenStatus = registerResult.status } @@ -56,7 +47,10 @@ internal class PushTokenManager( return PushTokenResponse(pushToken, pushTokenStatus) } - private fun pushStatusRuntimeError(status: SubscriptionStatus): Boolean { - return status.value < -6 - } + private fun shouldUpdateErrorStatus(newStatus: SubscriptionStatus): Boolean = + when { + !newStatus.isRetryableTokenError -> true + pushToken != null -> false + else -> pushTokenStatus == SubscriptionStatus.NO_PERMISSION || pushTokenStatus.isRetryableTokenError + } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt index 6ff846944a..73e2bb12eb 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt @@ -134,6 +134,8 @@ internal abstract class PushRegistratorAbstractGoogle( registrationId, SubscriptionStatus.SUBSCRIBED, ) + } catch (e: FCMSenderIdMismatchException) { + return invalidSenderIdResult(e) } catch (e: IOException) { val pushStatus: SubscriptionStatus = pushStatusFromThrowable(e) val exceptionMessage: String? = AndroidUtils.getRootCauseMessage(e) @@ -170,6 +172,14 @@ internal abstract class PushRegistratorAbstractGoogle( return null } + private fun invalidSenderIdResult(exception: FCMSenderIdMismatchException): IPushRegistrator.RegisterResult { + Logging.warn("FCM sender ID mismatch", exception) + return IPushRegistrator.RegisterResult( + null, + SubscriptionStatus.INVALID_FCM_SENDER_ID, + ) + } + private fun pushStatusFromThrowable(throwable: Throwable): SubscriptionStatus { val exceptionMessage: String? = AndroidUtils.getRootCauseMessage(throwable) return if (throwable is IOException) { diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index 697011537c..aba8600142 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -133,6 +133,8 @@ internal class PushRegistratorFCM( } } +internal class FCMSenderIdMismatchException(message: String) : IllegalStateException(message) + internal object FCMTokenProvider { fun firebaseAppSenderId( senderId: String?, @@ -208,7 +210,7 @@ internal object FCMTokenProvider { ) { if (firebaseAppSenderId == senderId) return - throw IllegalStateException( + throw FCMSenderIdMismatchException( "The default FirebaseApp uses sender id $firebaseAppSenderId, while OneSignal is " + "configured with sender id $senderId. Point both at the same Firebase project.", ) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/pushtoken/PushTokenManagerTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/pushtoken/PushTokenManagerTests.kt index ea2f1f5a44..6bc300324d 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/pushtoken/PushTokenManagerTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/pushtoken/PushTokenManagerTests.kt @@ -82,6 +82,24 @@ class PushTokenManagerTests : FunSpec({ pushTokenStatus shouldBe SubscriptionStatus.SUBSCRIBED } + test("permanent configuration errors replace a previously successful token status") { + val mockPushRegistrator = mockk() + coEvery { mockPushRegistrator.registerForPush() } returns + IPushRegistrator.RegisterResult("host-fid", SubscriptionStatus.SUBSCRIBED) andThen + IPushRegistrator.RegisterResult(null, SubscriptionStatus.INVALID_FCM_SENDER_ID) + val mockDeviceService = MockHelper.deviceService() + every { mockDeviceService.jetpackLibraryStatus } returns IDeviceService.JetpackLibraryStatus.OK + val pushTokenManager = PushTokenManager(mockPushRegistrator, mockDeviceService) + + pushTokenManager.retrievePushToken() + val response = pushTokenManager.retrievePushToken() + + response.token shouldBe null + response.status shouldBe SubscriptionStatus.INVALID_FCM_SENDER_ID + pushTokenManager.pushToken shouldBe null + pushTokenManager.pushTokenStatus shouldBe SubscriptionStatus.INVALID_FCM_SENDER_ID + } + test("retrievePushToken should fail with failure status from push registrator with config-type error") { // Given val mockPushRegistrator = mockk() diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt index 996674e6d8..00457db4ba 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt @@ -211,19 +211,27 @@ class PushRegistratorFCMTests : FunSpec({ thrown.message!! shouldContain "firebase_messaging_installation_id_enabled=not set" } - test("does not register against a default FirebaseApp with a different sender id") { + test("reports an invalid sender id when FID registration would use a different Firebase project") { + val configModelStore = + MockHelper.configModelStore { + it.isInitializedWithRemote = true + it.googleProjectNumber = SENDER_ID + } + val deviceService = mockk() + every { deviceService.hasFCMLibrary } returns true + every { deviceService.isGMSInstalledAndEnabled } returns true val registrator = registrator( legacyToken = Tasks.forException(disabledLegacyApi), installedApps = listOf(defaultApp("999999999999")), + configModelStore = configModelStore, + deviceService = deviceService, ) - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { registrator.getToken(SENDER_ID) } - } + val result = withContext(Dispatchers.IO) { registrator.registerForPush() } - thrown.message!! shouldContain "sender id 999999999999" + result.id shouldBe null + result.status shouldBe SubscriptionStatus.INVALID_FCM_SENDER_ID } test("uses OneSignal's FirebaseApp for a legacy token when the default app has a different sender id") {