Skip to content

Commit 8070632

Browse files
authored
feat: Core: AuthFlowController (#2252)
* feat: Core: AuthFlowController * example app * fix emulator * clean merge
1 parent 3b776cc commit 8070632

File tree

10 files changed

+2266
-202
lines changed

10 files changed

+2266
-202
lines changed

auth/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
<data android:scheme="@string/facebook_login_protocol_scheme" />
123123
</intent-filter>
124124
</activity>
125+
125126
<provider
126127
android:name=".data.client.AuthUiInitProvider"
127128
android:authorities="${applicationId}.authuiinitprovider"
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.firebase.ui.auth.compose
16+
17+
import android.app.Activity
18+
import android.content.Context
19+
import android.content.Intent
20+
import androidx.activity.result.ActivityResultLauncher
21+
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
22+
import kotlinx.coroutines.CoroutineScope
23+
import kotlinx.coroutines.Dispatchers
24+
import kotlinx.coroutines.Job
25+
import kotlinx.coroutines.cancel
26+
import kotlinx.coroutines.flow.Flow
27+
import kotlinx.coroutines.flow.launchIn
28+
import kotlinx.coroutines.flow.onEach
29+
import java.util.concurrent.atomic.AtomicBoolean
30+
31+
/**
32+
* Controller for managing the Firebase authentication flow lifecycle.
33+
*
34+
* This controller provides a lifecycle-safe way to start, monitor, and cancel
35+
* the authentication flow. It handles coroutine lifecycle, state listeners,
36+
* and resource cleanup automatically.
37+
*
38+
* **Usage Pattern:**
39+
* ```kotlin
40+
* class MyActivity : ComponentActivity() {
41+
* private lateinit var authController: AuthFlowController
42+
*
43+
* private val authLauncher = registerForActivityResult(
44+
* ActivityResultContracts.StartActivityForResult()
45+
* ) { result ->
46+
* // Auth flow completed
47+
* }
48+
*
49+
* override fun onCreate(savedInstanceState: Bundle?) {
50+
* super.onCreate(savedInstanceState)
51+
*
52+
* val authUI = FirebaseAuthUI.getInstance()
53+
* val configuration = authUIConfiguration {
54+
* providers = listOf(
55+
* AuthProvider.Email(),
56+
* AuthProvider.Google(...)
57+
* )
58+
* }
59+
*
60+
* authController = authUI.createAuthFlow(configuration)
61+
*
62+
* // Observe auth state
63+
* lifecycleScope.launch {
64+
* authController.authStateFlow.collect { state ->
65+
* when (state) {
66+
* is AuthState.Success -> {
67+
* // User signed in successfully
68+
* val user = state.user
69+
* }
70+
* is AuthState.Error -> {
71+
* // Handle error
72+
* }
73+
* is AuthState.Cancelled -> {
74+
* // User cancelled
75+
* }
76+
* else -> {}
77+
* }
78+
* }
79+
* }
80+
*
81+
* // Start auth flow
82+
* val intent = authController.createIntent(this)
83+
* authLauncher.launch(intent)
84+
* }
85+
*
86+
* override fun onDestroy() {
87+
* super.onDestroy()
88+
* authController.dispose()
89+
* }
90+
* }
91+
* ```
92+
*
93+
* **Lifecycle Management:**
94+
* - [createIntent] - Generate Intent to start the auth flow Activity
95+
* - [start] - Alternative to launch the flow (for Activity context)
96+
* - [cancel] - Cancel the ongoing auth flow, transitions to [AuthState.Cancelled]
97+
* - [dispose] - Release all resources (coroutines, listeners). Call in onDestroy()
98+
*
99+
* @property authUI The [FirebaseAuthUI] instance managing authentication
100+
* @property configuration The [AuthUIConfiguration] defining the auth flow behavior
101+
*
102+
* @since 10.0.0
103+
*/
104+
class AuthFlowController internal constructor(
105+
private val authUI: FirebaseAuthUI,
106+
private val configuration: AuthUIConfiguration
107+
) {
108+
109+
private val coroutineScope = CoroutineScope(Dispatchers.Main + Job())
110+
private val isDisposed = AtomicBoolean(false)
111+
private var stateCollectionJob: Job? = null
112+
113+
/**
114+
* Flow of [AuthState] changes during the authentication flow.
115+
*
116+
* Subscribe to this flow to observe authentication state changes.
117+
* The flow is backed by the [FirebaseAuthUI.authStateFlow] and will
118+
* emit states like:
119+
* - [AuthState.Idle] - No active authentication
120+
* - [AuthState.Loading] - Authentication in progress
121+
* - [AuthState.Success] - User signed in successfully
122+
* - [AuthState.Error] - Authentication error occurred
123+
* - [AuthState.Cancelled] - User cancelled the flow
124+
* - [AuthState.RequiresMfa] - Multi-factor authentication required
125+
* - [AuthState.RequiresEmailVerification] - Email verification required
126+
*/
127+
val authStateFlow: Flow<AuthState>
128+
get() {
129+
checkNotDisposed()
130+
return authUI.authStateFlow()
131+
}
132+
133+
/**
134+
* Creates an Intent to launch the Firebase authentication flow.
135+
*
136+
* Use this method with [ActivityResultLauncher] to start the auth flow
137+
* and handle the result in a lifecycle-aware manner.
138+
*
139+
* **Example:**
140+
* ```kotlin
141+
* val authLauncher = registerForActivityResult(
142+
* ActivityResultContracts.StartActivityForResult()
143+
* ) { result ->
144+
* if (result.resultCode == Activity.RESULT_OK) {
145+
* // Auth flow completed successfully
146+
* } else {
147+
* // Auth flow cancelled or error
148+
* }
149+
* }
150+
*
151+
* val intent = authController.createIntent(this)
152+
* authLauncher.launch(intent)
153+
* ```
154+
*
155+
* @param context Android [Context] to create the Intent
156+
* @return [Intent] configured to launch the auth flow Activity
157+
* @throws IllegalStateException if the controller has been disposed
158+
*/
159+
fun createIntent(context: Context): Intent {
160+
checkNotDisposed()
161+
return FirebaseAuthActivity.createIntent(context, configuration)
162+
}
163+
164+
/**
165+
* Starts the Firebase authentication flow.
166+
*
167+
* This method launches the auth flow Activity from the provided [Activity] context.
168+
* For better lifecycle management, prefer using [createIntent] with
169+
* [ActivityResultLauncher] instead.
170+
*
171+
* **Note:** This method uses [Activity.startActivityForResult] which is deprecated.
172+
* Consider using [createIntent] with the Activity Result API instead.
173+
*
174+
* @param activity The [Activity] to launch from
175+
* @param requestCode Request code for [Activity.onActivityResult]
176+
* @throws IllegalStateException if the controller has been disposed
177+
*
178+
* @see createIntent
179+
*/
180+
@Deprecated(
181+
message = "Use createIntent() with ActivityResultLauncher instead",
182+
replaceWith = ReplaceWith("createIntent(activity)"),
183+
level = DeprecationLevel.WARNING
184+
)
185+
fun start(activity: Activity, requestCode: Int = RC_SIGN_IN) {
186+
checkNotDisposed()
187+
val intent = createIntent(activity)
188+
activity.startActivityForResult(intent, requestCode)
189+
}
190+
191+
/**
192+
* Cancels the ongoing authentication flow.
193+
*
194+
* This method transitions the auth state to [AuthState.Cancelled] and
195+
* signals the auth flow to terminate. The auth flow Activity will finish
196+
* and return [Activity.RESULT_CANCELED].
197+
*
198+
* **Example:**
199+
* ```kotlin
200+
* // User clicked a "Cancel" button
201+
* cancelButton.setOnClickListener {
202+
* authController.cancel()
203+
* }
204+
* ```
205+
*
206+
* @throws IllegalStateException if the controller has been disposed
207+
*/
208+
fun cancel() {
209+
checkNotDisposed()
210+
authUI.updateAuthState(AuthState.Cancelled)
211+
}
212+
213+
/**
214+
* Disposes the controller and releases all resources.
215+
*
216+
* This method:
217+
* - Cancels all coroutines in the controller scope
218+
* - Stops listening to auth state changes
219+
* - Marks the controller as disposed
220+
*
221+
* Call this method in your Activity's `onDestroy()` to prevent memory leaks.
222+
*
223+
* **Important:** Once disposed, this controller cannot be reused. Create a new
224+
* controller if you need to start another auth flow.
225+
*
226+
* **Example:**
227+
* ```kotlin
228+
* override fun onDestroy() {
229+
* super.onDestroy()
230+
* authController.dispose()
231+
* }
232+
* ```
233+
*
234+
* @throws IllegalStateException if already disposed (when called multiple times)
235+
*/
236+
fun dispose() {
237+
if (isDisposed.compareAndSet(false, true)) {
238+
stateCollectionJob?.cancel()
239+
coroutineScope.cancel()
240+
}
241+
}
242+
243+
/**
244+
* Checks if the controller has been disposed.
245+
*
246+
* @return `true` if disposed, `false` otherwise
247+
*/
248+
fun isDisposed(): Boolean = isDisposed.get()
249+
250+
private fun checkNotDisposed() {
251+
check(!isDisposed.get()) {
252+
"AuthFlowController has been disposed. Create a new controller to start another auth flow."
253+
}
254+
}
255+
256+
internal fun startStateCollection() {
257+
if (stateCollectionJob == null || stateCollectionJob?.isActive == false) {
258+
stateCollectionJob = authUI.authStateFlow()
259+
.onEach { state ->
260+
// Optional: Add logging or side effects here
261+
}
262+
.launchIn(coroutineScope)
263+
}
264+
}
265+
266+
companion object {
267+
/**
268+
* Request code for the sign-in activity result.
269+
*
270+
* Use this constant when calling [start] with `startActivityForResult`.
271+
*/
272+
const val RC_SIGN_IN = 9001
273+
}
274+
}

0 commit comments

Comments
 (0)