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 @@ -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
)
}

/**
Expand Down
52 changes: 43 additions & 9 deletions auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -72,15 +73,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 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
launchKey = intent.getStringExtra(EXTRA_CONFIGURATION_KEY)
configuration = if (launchKey != null) {
configurationCache[launchKey]
} else {
null
} ?: run {
Expand All @@ -90,7 +92,16 @@ class FirebaseAuthActivity : ComponentActivity() {
return
}

authUI = FirebaseAuthUI.getInstance()
authUI = if (launchKey != null) {
authUICache[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)
Expand Down Expand Up @@ -150,11 +161,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 {
Expand Down Expand Up @@ -191,14 +208,31 @@ 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)
}
}

/**
* 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<String, FirebaseAuthUI>()
}
}
84 changes: 72 additions & 12 deletions auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -98,6 +111,7 @@ class FirebaseAuthActivityTest {

@After
fun tearDown() {
FirebaseAuthActivity.clearLaunchStateCache()
FirebaseAuthUI.clearInstanceCache()
FirebaseApp.getApps(applicationContext).forEach { app ->
try {
Expand Down Expand Up @@ -180,6 +194,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
// =============================================================================================
Expand Down Expand Up @@ -394,22 +448,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
Expand Down
Loading