Skip to content

Commit 19d1d61

Browse files
committed
fix(auth): only exit auth flow on AuthState.Finished, not per-provider Cancelled
1 parent c4f4314 commit 19d1d61

5 files changed

Lines changed: 64 additions & 18 deletions

File tree

auth/src/main/java/com/firebase/ui/auth/AuthFlowController.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ import java.util.concurrent.atomic.AtomicBoolean
7070
* is AuthState.Error -> {
7171
* // Handle error
7272
* }
73-
* is AuthState.Cancelled -> {
74-
* // User cancelled
73+
* is AuthState.Finished -> {
74+
* // Flow was ended via authController.cancel()
7575
* }
7676
* else -> {}
7777
* }
@@ -93,7 +93,7 @@ import java.util.concurrent.atomic.AtomicBoolean
9393
* **Lifecycle Management:**
9494
* - [createIntent] - Generate Intent to start the auth flow Activity
9595
* - [start] - Alternative to launch the flow (for Activity context)
96-
* - [cancel] - Cancel the ongoing auth flow, transitions to [AuthState.Cancelled]
96+
* - [cancel] - End the ongoing auth flow, transitions to [AuthState.Finished]
9797
* - [dispose] - Release all resources (coroutines, listeners). Call in onDestroy()
9898
*
9999
* @property authUI The [FirebaseAuthUI] instance managing authentication
@@ -120,7 +120,8 @@ class AuthFlowController internal constructor(
120120
* - [AuthState.Loading] - Authentication in progress
121121
* - [AuthState.Success] - User signed in successfully
122122
* - [AuthState.Error] - Authentication error occurred
123-
* - [AuthState.Cancelled] - User cancelled the flow
123+
* - [AuthState.Cancelled] - User cancelled a single sign-in attempt, flow stays open
124+
* - [AuthState.Finished] - Flow was ended via [cancel]
124125
* - [AuthState.RequiresMfa] - Multi-factor authentication required
125126
* - [AuthState.RequiresEmailVerification] - Email verification required
126127
*/
@@ -195,7 +196,7 @@ class AuthFlowController internal constructor(
195196
/**
196197
* Cancels the ongoing authentication flow.
197198
*
198-
* This method transitions the auth state to [AuthState.Cancelled] and
199+
* This method transitions the auth state to [AuthState.Finished] and
199200
* signals the auth flow to terminate. The auth flow Activity will finish
200201
* and return [Activity.RESULT_CANCELED].
201202
*
@@ -211,7 +212,7 @@ class AuthFlowController internal constructor(
211212
*/
212213
fun cancel() {
213214
checkNotDisposed()
214-
authUI.updateAuthState(AuthState.Cancelled)
215+
authUI.updateAuthState(AuthState.Finished)
215216
}
216217

217218
/**

auth/src/main/java/com/firebase/ui/auth/AuthState.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,33 @@ abstract class AuthState private constructor() {
120120

121121
/**
122122
* Authentication was cancelled by the user.
123+
*
124+
* This is a normal, in-flow outcome (e.g. dismissing a provider's sign-in sheet) -
125+
* it does not end the authentication flow. UI hosting the flow resets to its
126+
* starting screen rather than exiting.
127+
*
128+
* @see Finished for programmatically ending the whole flow
123129
*/
124130
class Cancelled internal constructor() : AuthState() {
125131
override fun equals(other: Any?): Boolean = other is Cancelled
126132
override fun hashCode(): Int = javaClass.hashCode()
127133
override fun toString(): String = "AuthState.Cancelled"
128134
}
129135

136+
/**
137+
* The authentication flow was ended programmatically (e.g. via
138+
* [AuthFlowController.cancel]) and should exit entirely.
139+
*
140+
* Unlike [Cancelled], which represents the user backing out of a single
141+
* sign-in attempt while staying in the flow, [Finished] terminates the whole
142+
* flow - the hosting Activity finishes with `RESULT_CANCELED`.
143+
*/
144+
class Finished internal constructor() : AuthState() {
145+
override fun equals(other: Any?): Boolean = other is Finished
146+
override fun hashCode(): Int = javaClass.hashCode()
147+
override fun toString(): String = "AuthState.Finished"
148+
}
149+
130150
/**
131151
* Multi-factor authentication is required to complete sign-in.
132152
*
@@ -337,5 +357,12 @@ abstract class AuthState private constructor() {
337357
*/
338358
@JvmStatic
339359
val Cancelled: Cancelled = Cancelled()
360+
361+
/**
362+
* Creates a Finished state instance.
363+
* @return A new [Finished] state
364+
*/
365+
@JvmStatic
366+
val Finished: Finished = Finished()
340367
}
341368
}

auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import java.util.concurrent.ConcurrentHashMap
3636
*
3737
* This activity displays the [FirebaseAuthScreen] composable and manages
3838
* the authentication flow lifecycle. It automatically finishes when the user
39-
* signs in successfully or cancels the flow.
39+
* signs in successfully or the flow is ended (e.g. via [AuthFlowController.cancel]).
40+
* A user backing out of a single provider's sign-in attempt does not finish the
41+
* activity - it resets to the starting screen instead.
4042
*
4143
* **Do not launch this Activity directly.**
4244
* Use [AuthFlowController] to start the auth flow:
@@ -116,8 +118,8 @@ class FirebaseAuthActivity : ComponentActivity() {
116118
setResult(RESULT_OK, resultIntent)
117119
finish()
118120
}
119-
is AuthState.Cancelled -> {
120-
// User cancelled the flow
121+
is AuthState.Finished -> {
122+
// Flow was ended programmatically (e.g. AuthFlowController.cancel())
121123
setResult(RESULT_CANCELED)
122124
finish()
123125
}

auth/src/test/java/com/firebase/ui/auth/AuthFlowControllerTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ class AuthFlowControllerTest {
201201
// =============================================================================================
202202

203203
@Test
204-
fun `cancel() updates state to Cancelled`() = runTest {
204+
fun `cancel() updates state to Finished`() = runTest {
205205
val controller = authUI.createAuthFlow(configuration)
206206

207207
// Cancel the flow
@@ -213,8 +213,8 @@ class AuthFlowControllerTest {
213213
// Collect first state after cancel
214214
val state = controller.authStateFlow.first()
215215

216-
// Should be Cancelled state
217-
assertThat(state).isInstanceOf(AuthState.Cancelled::class.java)
216+
// Should be Finished state
217+
assertThat(state).isInstanceOf(AuthState.Finished::class.java)
218218
}
219219

220220
@Test
@@ -438,9 +438,9 @@ class AuthFlowControllerTest {
438438
// Advance test scheduler to process all pending coroutines
439439
testScheduler.advanceUntilIdle()
440440

441-
// Verify cancelled state
441+
// Verify finished state
442442
val state = controller.authStateFlow.first()
443-
assertThat(state).isInstanceOf(AuthState.Cancelled::class.java)
443+
assertThat(state).isInstanceOf(AuthState.Finished::class.java)
444444

445445
// Dispose
446446
controller.dispose()

auth/src/test/java/com/firebase/ui/auth/FirebaseAuthActivityTest.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,18 +313,18 @@ class FirebaseAuthActivityTest {
313313
}
314314

315315
// =============================================================================================
316-
// Auth State Cancelled Tests
316+
// Auth State Cancelled/Finished Tests
317317
// =============================================================================================
318318

319319
@Test
320-
fun `activity finishes with RESULT_CANCELED on Cancelled state`() = runTest {
320+
fun `activity finishes with RESULT_CANCELED on Finished state`() = runTest {
321321
val intent = FirebaseAuthActivity.createIntent(applicationContext, configuration)
322322
val controller = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent)
323323

324324
val activity = controller.create().start().resume().get()
325325

326-
// Update to Cancelled state
327-
authUI.updateAuthState(AuthState.Cancelled)
326+
// Update to Finished state (e.g. AuthFlowController.cancel())
327+
authUI.updateAuthState(AuthState.Finished)
328328

329329
shadowOf(Looper.getMainLooper()).idle()
330330

@@ -336,6 +336,22 @@ class FirebaseAuthActivityTest {
336336
assertThat(shadowActivity.resultCode).isEqualTo(Activity.RESULT_CANCELED)
337337
}
338338

339+
@Test
340+
fun `activity does not finish on Cancelled state`() = runTest {
341+
val intent = FirebaseAuthActivity.createIntent(applicationContext, configuration)
342+
val controller = Robolectric.buildActivity(FirebaseAuthActivity::class.java, intent)
343+
344+
val activity = controller.create().start().resume().get()
345+
346+
// User backed out of a single provider's sign-in attempt (e.g. dismissed
347+
// the Google Credential Manager sheet) - the flow should stay open.
348+
authUI.updateAuthState(AuthState.Cancelled)
349+
350+
shadowOf(Looper.getMainLooper()).idle()
351+
352+
assertThat(activity.isFinishing).isFalse()
353+
}
354+
339355
// =============================================================================================
340356
// Auth State Error Tests
341357
// =============================================================================================

0 commit comments

Comments
 (0)