@@ -55,12 +55,13 @@ import com.google.firebase.auth.PhoneAuthProvider
5555import com.google.firebase.auth.TwitterAuthProvider
5656import com.google.firebase.auth.UserProfileChangeRequest
5757import com.google.firebase.auth.actionCodeSettings
58+ import kotlinx.coroutines.channels.awaitClose
59+ import kotlinx.coroutines.flow.Flow
60+ import kotlinx.coroutines.flow.callbackFlow
5861import kotlinx.coroutines.suspendCancellableCoroutine
5962import kotlinx.coroutines.tasks.await
6063import java.util.concurrent.TimeUnit
6164import kotlin.coroutines.resume
62- import kotlin.coroutines.resumeWithException
63- import kotlin.coroutines.suspendCoroutine
6465
6566@AuthUIConfigurationDsl
6667class AuthProvidersBuilder {
@@ -336,19 +337,23 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
336337 }
337338
338339 /* *
339- * Internal coroutine-based wrapper for Firebase Phone Authentication verification.
340+ * Internal wrapper that exposes Firebase Phone Authentication verification as a [Flow] .
340341 *
341- * This method wraps the callback-based Firebase Phone Auth API into a suspending function
342- * using Kotlin coroutines. It handles the Firebase [PhoneAuthProvider.OnVerificationStateChangedCallbacks]
343- * and converts them into a [VerifyPhoneNumberResult].
342+ * Firebase's [PhoneAuthProvider.OnVerificationStateChangedCallbacks] is a multi-shot
343+ * callback: for the same request it can report `onCodeSent` and then, once the SMS is
344+ * auto-retrieved, `onVerificationCompleted`. Each callback becomes one emission, so no
345+ * result is ever dropped.
344346 *
345347 * **Callback mapping:**
346348 * - `onVerificationCompleted` → [VerifyPhoneNumberResult.AutoVerified]
347349 * - `onCodeSent` → [VerifyPhoneNumberResult.NeedsManualVerification]
348- * - `onVerificationFailed` → throws the exception
350+ * - `onVerificationFailed` → terminates the flow with that exception
351+ * - `onCodeAutoRetrievalTimeOut` → completes the flow normally
349352 *
350- * This is a private helper method used by [verifyPhoneNumber]. Callers should use
351- * [verifyPhoneNumber] instead as it handles state management and error handling.
353+ * `onCodeAutoRetrievalTimeOut` fires only when the window expires without a prior
354+ * `onVerificationCompleted`, so the flow terminates on its own only on the SMS path.
355+ * Instant verification has no terminal callback: there the flow stays open until the
356+ * collector is cancelled. Callers that only want the first result should use `first()`.
352357 *
353358 * @param auth The [FirebaseAuth] instance to use for verification
354359 * @param phoneNumber The phone number to verify in E.164 format
@@ -357,17 +362,16 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
357362 * instead of primary sign-in. Pass null for standard phone authentication.
358363 * @param forceResendingToken Optional token from previous verification for resending
359364 *
360- * @return [VerifyPhoneNumberResult] indicating auto-verified or manual verification needed
361- * @throws FirebaseException if verification fails
365+ * @return a [Flow] of [VerifyPhoneNumberResult] emissions, one per Firebase callback
362366 */
363- internal suspend fun verifyPhoneNumberAwait (
367+ internal fun verifyPhoneNumberFlow (
364368 auth : FirebaseAuth ,
365369 activity : Activity ? ,
366370 phoneNumber : String ,
367371 multiFactorSession : MultiFactorSession ? = null,
368372 forceResendingToken : PhoneAuthProvider .ForceResendingToken ? ,
369373 verifier : Verifier = DefaultVerifier (),
370- ): VerifyPhoneNumberResult {
374+ ): Flow < VerifyPhoneNumberResult > {
371375 return verifier.verifyPhoneNumber(
372376 auth,
373377 activity,
@@ -383,71 +387,78 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
383387 * @suppress
384388 */
385389 internal interface Verifier {
386- suspend fun verifyPhoneNumber (
390+ fun verifyPhoneNumber (
387391 auth : FirebaseAuth ,
388392 activity : Activity ? ,
389393 phoneNumber : String ,
390394 timeout : Long ,
391395 forceResendingToken : PhoneAuthProvider .ForceResendingToken ? ,
392396 multiFactorSession : MultiFactorSession ? ,
393397 isInstantVerificationEnabled : Boolean ,
394- ): VerifyPhoneNumberResult
398+ ): Flow < VerifyPhoneNumberResult >
395399 }
396400
397401 /* *
398402 * @suppress
399403 */
400404 internal class DefaultVerifier : Verifier {
401- override suspend fun verifyPhoneNumber (
405+ override fun verifyPhoneNumber (
402406 auth : FirebaseAuth ,
403407 activity : Activity ? ,
404408 phoneNumber : String ,
405409 timeout : Long ,
406410 forceResendingToken : PhoneAuthProvider .ForceResendingToken ? ,
407411 multiFactorSession : MultiFactorSession ? ,
408412 isInstantVerificationEnabled : Boolean ,
409- ): VerifyPhoneNumberResult {
410- return suspendCoroutine { continuation ->
411- val options = PhoneAuthOptions .newBuilder(auth)
412- .setPhoneNumber(phoneNumber)
413- .requireSmsValidation(! isInstantVerificationEnabled)
414- .setTimeout(timeout, TimeUnit .SECONDS )
415- .setCallbacks(object :
416- PhoneAuthProvider .OnVerificationStateChangedCallbacks () {
417- override fun onVerificationCompleted (credential : PhoneAuthCredential ) {
418- continuation.resume(VerifyPhoneNumberResult .AutoVerified (credential))
419- }
413+ ): Flow <VerifyPhoneNumberResult > = callbackFlow {
414+ val options = PhoneAuthOptions .newBuilder(auth)
415+ .setPhoneNumber(phoneNumber)
416+ .requireSmsValidation(! isInstantVerificationEnabled)
417+ .setTimeout(timeout, TimeUnit .SECONDS )
418+ .setCallbacks(object :
419+ PhoneAuthProvider .OnVerificationStateChangedCallbacks () {
420+ override fun onVerificationCompleted (credential : PhoneAuthCredential ) {
421+ trySend(VerifyPhoneNumberResult .AutoVerified (credential))
422+ }
420423
421- override fun onVerificationFailed (e : FirebaseException ) {
422- continuation.resumeWithException (e)
423- }
424+ override fun onVerificationFailed (e : FirebaseException ) {
425+ close (e)
426+ }
424427
425- override fun onCodeSent (
426- verificationId : String ,
427- token : PhoneAuthProvider .ForceResendingToken ,
428- ) {
429- continuation.resume(
430- VerifyPhoneNumberResult .NeedsManualVerification (
431- verificationId,
432- token
433- )
428+ override fun onCodeSent (
429+ verificationId : String ,
430+ token : PhoneAuthProvider .ForceResendingToken ,
431+ ) {
432+ trySend(
433+ VerifyPhoneNumberResult .NeedsManualVerification (
434+ verificationId,
435+ token
434436 )
435- }
436- })
437- .apply {
438- activity?.let {
439- setActivity(it)
440- }
441- forceResendingToken?.let {
442- setForceResendingToken(it)
443- }
444- multiFactorSession?.let {
445- setMultiFactorSession(it)
446- }
437+ )
447438 }
448- .build()
449- PhoneAuthProvider .verifyPhoneNumber(options)
450- }
439+
440+ // Firebase's own terminal: nothing further can arrive for this request,
441+ // so complete rather than leaving the collector waiting forever.
442+ override fun onCodeAutoRetrievalTimeOut (verificationId : String ) {
443+ close()
444+ }
445+ })
446+ .apply {
447+ activity?.let {
448+ setActivity(it)
449+ }
450+ forceResendingToken?.let {
451+ setForceResendingToken(it)
452+ }
453+ multiFactorSession?.let {
454+ setMultiFactorSession(it)
455+ }
456+ }
457+ .build()
458+ PhoneAuthProvider .verifyPhoneNumber(options)
459+ // Firebase exposes no way to unregister these callbacks, so there is nothing to
460+ // tear down when the collector goes away.
461+ awaitClose { }
451462 }
452463 }
453464
0 commit comments