From 62b80d089c5a0075caee391ec1c40b7b18a3b97f Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 1 Apr 2026 13:59:32 +0100 Subject: [PATCH 1/4] fix: use secondary app if it is passed into FirebaseAuthUI --- .../firebase/ui/auth/AuthFlowController.kt | 6 ++++- .../firebase/ui/auth/FirebaseAuthActivity.kt | 25 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthFlowController.kt b/auth/src/main/java/com/firebase/ui/auth/AuthFlowController.kt index 44cdf45aa..917584219 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthFlowController.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthFlowController.kt @@ -158,7 +158,11 @@ class AuthFlowController internal constructor( */ fun createIntent(context: Context): Intent { checkNotDisposed() - return FirebaseAuthActivity.createIntent(context, configuration) + return FirebaseAuthActivity.createIntent( + context = context, + configuration = configuration, + authUI = authUI + ) } /** diff --git a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt index 168670da1..92a24ff39 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt @@ -77,10 +77,10 @@ class FirebaseAuthActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() - // Extract configuration from cache using UUID key - val configKey = intent.getStringExtra(EXTRA_CONFIGURATION_KEY) - configuration = if (configKey != null) { - configurationCache.remove(configKey) + // Extract configuration and auth instance from cache using UUID key + val launchKey = intent.getStringExtra(EXTRA_CONFIGURATION_KEY) + configuration = if (launchKey != null) { + configurationCache.remove(launchKey) } else { null } ?: run { @@ -90,7 +90,16 @@ class FirebaseAuthActivity : ComponentActivity() { return } - authUI = FirebaseAuthUI.getInstance() + authUI = if (launchKey != null) { + authUICache.remove(launchKey) + } else { + null + } ?: run { + // Missing auth instance, finish with error + setResult(RESULT_CANCELED) + finish() + return + } // Extract email link if present val emailLink = intent.getStringExtra(EmailLinkConstants.EXTRA_EMAIL_LINK) @@ -191,14 +200,18 @@ class FirebaseAuthActivity : ComponentActivity() { */ internal fun createIntent( context: Context, - configuration: AuthUIConfiguration + configuration: AuthUIConfiguration, + authUI: FirebaseAuthUI = FirebaseAuthUI.getInstance() ): Intent { val configKey = UUID.randomUUID().toString() configurationCache[configKey] = configuration + authUICache[configKey] = authUI return Intent(context, FirebaseAuthActivity::class.java).apply { putExtra(EXTRA_CONFIGURATION_KEY, configKey) } } + + private val authUICache = ConcurrentHashMap() } } From d8c7424a147a6fe610911c133b3ecc2d5c78304d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 1 Apr 2026 14:00:04 +0100 Subject: [PATCH 2/4] test: test secondary app is used via intent --- .../ui/auth/FirebaseAuthActivityTest.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt index 06e8c972a..12c4f6f6d 100644 --- a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt @@ -50,6 +50,7 @@ class FirebaseAuthActivityTest { private lateinit var applicationContext: Context private lateinit var authUI: FirebaseAuthUI + private lateinit var secondaryAuthUI: FirebaseAuthUI private lateinit var configuration: AuthUIConfiguration @Mock @@ -79,8 +80,20 @@ class FirebaseAuthActivityTest { .build() ) + val secondaryApp = FirebaseApp.initializeApp( + applicationContext, + FirebaseOptions.Builder() + .setApiKey("fake-api-key-2") + .setApplicationId("fake-app-id-2") + .setProjectId("fake-project-id-2") + .build(), + "secondary" + ) + authUI = FirebaseAuthUI.getInstance() authUI.auth.useEmulator("127.0.0.1", 9099) + secondaryAuthUI = FirebaseAuthUI.getInstance(secondaryApp) + secondaryAuthUI.auth.useEmulator("127.0.0.1", 9099) configuration = AuthUIConfiguration( context = applicationContext, @@ -180,6 +193,46 @@ class FirebaseAuthActivityTest { assertThat(activity.isFinishing).isFalse() } + @Test + fun `activity launched from secondary auth flow observes supplied authUI instead of default app`() = + runTest { + val controller = secondaryAuthUI.createAuthFlow(configuration) + val intent = controller.createIntent(applicationContext) + val activity = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent) + .create() + .start() + .resume() + .get() + + `when`(mockFirebaseUser.uid).thenReturn("secondary-user-id") + + authUI.updateAuthState( + AuthState.Success( + result = null, + user = mockFirebaseUser, + isNewUser = false + ) + ) + shadowOf(Looper.getMainLooper()).idle() + + assertThat(activity.isFinishing).isFalse() + + secondaryAuthUI.updateAuthState( + AuthState.Success( + result = null, + user = mockFirebaseUser, + isNewUser = false + ) + ) + shadowOf(Looper.getMainLooper()).idle() + + assertThat(activity.isFinishing).isTrue() + val shadowActivity = shadowOf(activity) + assertThat(shadowActivity.resultCode).isEqualTo(Activity.RESULT_OK) + assertThat(shadowActivity.resultIntent.getStringExtra(FirebaseAuthActivity.EXTRA_USER_ID)) + .isEqualTo("secondary-user-id") + } + // ============================================================================================= // Auth State Success Tests // ============================================================================================= From ce8f56b497021ebf23a3b1a3c8d4e0f8bfa8f96a Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 1 Apr 2026 17:03:20 +0100 Subject: [PATCH 3/4] fix(auth): preserve cached auth flow state across activity recreation --- .../firebase/ui/auth/FirebaseAuthActivity.kt | 19 ++++++++---- .../ui/auth/FirebaseAuthActivityTest.kt | 30 +++++++++++-------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt index 92a24ff39..318734d46 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt @@ -72,15 +72,16 @@ class FirebaseAuthActivity : ComponentActivity() { private lateinit var authUI: FirebaseAuthUI private lateinit var configuration: AuthUIConfiguration + private var launchKey: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() // Extract configuration and auth instance from cache using UUID key - val launchKey = intent.getStringExtra(EXTRA_CONFIGURATION_KEY) + launchKey = intent.getStringExtra(EXTRA_CONFIGURATION_KEY) configuration = if (launchKey != null) { - configurationCache.remove(launchKey) + configurationCache[launchKey] } else { null } ?: run { @@ -91,7 +92,7 @@ class FirebaseAuthActivity : ComponentActivity() { } authUI = if (launchKey != null) { - authUICache.remove(launchKey) + authUICache[launchKey] } else { null } ?: run { @@ -159,11 +160,17 @@ class FirebaseAuthActivity : ComponentActivity() { } override fun onDestroy() { - super.onDestroy() - // Reset auth state when activity is destroyed - if (!isFinishing) { + if (isFinishing) { + launchKey?.let { key -> + configurationCache.remove(key) + authUICache.remove(key) + } + } else { + // Preserve cached launch state so the recreated activity can recover it. authUI.updateAuthState(AuthState.Idle) } + + super.onDestroy() } companion object { diff --git a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt index 12c4f6f6d..b04280c69 100644 --- a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt @@ -447,22 +447,28 @@ class FirebaseAuthActivityTest { // ============================================================================================= @Test - fun `configuration is removed from cache after onCreate`() { - val intent1 = FirebaseAuthActivity.createIntent(applicationContext, configuration) - val configKey1 = intent1.getStringExtra("com.firebase.ui.auth.CONFIGURATION_KEY") + fun `launch state survives recreation and is cleared when activity finishes`() { + val intent = FirebaseAuthActivity.createIntent(applicationContext, configuration) - assertThat(configKey1).isNotNull() + val firstController = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent) + val firstActivity = firstController.create().start().resume().get() + assertThat(firstActivity.isFinishing).isFalse() - // Create activity - this should consume the configuration from cache - val controller1 = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent1) - controller1.create().get() + // Simulate recreation: the first activity is destroyed without finishing. + firstController.pause().stop().destroy() + + val recreatedController = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent) + val recreatedActivity = recreatedController.create().start().resume().get() + assertThat(recreatedActivity.isFinishing).isFalse() - // Create another intent - val intent2 = FirebaseAuthActivity.createIntent(applicationContext, configuration) - val configKey2 = intent2.getStringExtra("com.firebase.ui.auth.CONFIGURATION_KEY") + // Once the recreated activity actually finishes, the cached launch state should be released. + recreatedActivity.finish() + recreatedController.pause().stop().destroy() - // Should be a different key - assertThat(configKey2).isNotEqualTo(configKey1) + val postFinishController = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent) + val postFinishActivity = postFinishController.create().get() + assertThat(postFinishActivity.isFinishing).isTrue() + assertThat(shadowOf(postFinishActivity).resultCode).isEqualTo(Activity.RESULT_CANCELED) } @Test From c6c0c0325ef182b37db9e5c9c41149375e347def Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 1 Apr 2026 17:05:39 +0100 Subject: [PATCH 4/4] test(auth): clear FirebaseAuthActivity launch caches between tests --- .../com/firebase/ui/auth/FirebaseAuthActivity.kt | 14 ++++++++++++++ .../firebase/ui/auth/FirebaseAuthActivityTest.kt | 1 + 2 files changed, 15 insertions(+) diff --git a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt index 318734d46..30f4b3fbc 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt @@ -18,6 +18,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.annotation.RestrictTo import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -219,6 +220,19 @@ class FirebaseAuthActivity : ComponentActivity() { } } + /** + * Clears cached launch state. This method is intended for testing purposes only. + * + * @suppress This is an internal API and should not be used in production code. + * @RestrictTo RestrictTo.Scope.TESTS + */ + @JvmStatic + @RestrictTo(RestrictTo.Scope.TESTS) + fun clearLaunchStateCache() { + configurationCache.clear() + authUICache.clear() + } + private val authUICache = ConcurrentHashMap() } } diff --git a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt index b04280c69..a94999439 100644 --- a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt @@ -111,6 +111,7 @@ class FirebaseAuthActivityTest { @After fun tearDown() { + FirebaseAuthActivity.clearLaunchStateCache() FirebaseAuthUI.clearInstanceCache() FirebaseApp.getApps(applicationContext).forEach { app -> try {