@@ -28,17 +28,25 @@ import com.firebase.ui.auth.util.Preconditions
2828import com.firebase.ui.auth.util.data.ContinueUrlBuilder
2929import com.firebase.ui.auth.util.data.PhoneNumberUtils
3030import com.firebase.ui.auth.util.data.ProviderAvailability
31+ import com.google.firebase.FirebaseException
3132import com.google.firebase.auth.ActionCodeSettings
3233import com.google.firebase.auth.EmailAuthProvider
3334import com.google.firebase.auth.FacebookAuthProvider
3435import com.google.firebase.auth.FirebaseAuth
3536import com.google.firebase.auth.GithubAuthProvider
3637import com.google.firebase.auth.GoogleAuthProvider
38+ import com.google.firebase.auth.MultiFactorSession
39+ import com.google.firebase.auth.PhoneAuthCredential
40+ import com.google.firebase.auth.PhoneAuthOptions
3741import com.google.firebase.auth.PhoneAuthProvider
3842import com.google.firebase.auth.TwitterAuthProvider
3943import com.google.firebase.auth.UserProfileChangeRequest
4044import com.google.firebase.auth.actionCodeSettings
4145import kotlinx.coroutines.tasks.await
46+ import java.util.concurrent.TimeUnit
47+ import kotlin.coroutines.resume
48+ import kotlin.coroutines.resumeWithException
49+ import kotlin.coroutines.suspendCoroutine
4250
4351@AuthUIConfigurationDsl
4452class AuthProvidersBuilder {
@@ -202,9 +210,7 @@ abstract class AuthProvider(open val providerId: String) {
202210 val KEY_IDP_SECRET = stringPreferencesKey(" com.firebase.ui.auth.data.client.idpSecret" )
203211 }
204212
205- internal fun validate (
206- isAnonymousUpgradeEnabled : Boolean = false
207- ) {
213+ internal fun validate (isAnonymousUpgradeEnabled : Boolean = false) {
208214 if (isEmailLinkSignInEnabled) {
209215 val actionCodeSettings = requireNotNull(emailLinkActionCodeSettings) {
210216 " ActionCodeSettings cannot be null when using " +
@@ -304,8 +310,36 @@ abstract class AuthProvider(open val providerId: String) {
304310 /* *
305311 * Enables automatic retrieval of the SMS code. Defaults to true.
306312 */
313+ // TODO(demolaf): is this meant to be isSMSAutoFillInputFieldEnabled?
307314 val isAutoRetrievalEnabled : Boolean = true
308315 ) : AuthProvider(providerId = Provider .PHONE .id) {
316+ /* *
317+ * Sealed class representing the result of phone number verification.
318+ *
319+ * Phone verification can complete in two ways:
320+ * - [AutoVerified]: SMS was instantly retrieved and verified by the Firebase SDK
321+ * - [NeedsManualVerification]: SMS code was sent, user must manually enter it
322+ */
323+ internal sealed class VerifyPhoneNumberResult {
324+ /* *
325+ * Instant verification succeeded via SMS auto-retrieval.
326+ *
327+ * @property credential The [PhoneAuthCredential] that can be used to sign in
328+ */
329+ class AutoVerified (val credential : PhoneAuthCredential ) : VerifyPhoneNumberResult()
330+
331+ /* *
332+ * Instant verification failed, manual code entry required.
333+ *
334+ * @property verificationId The verification ID to use when submitting the code
335+ * @property token Token for resending the verification code
336+ */
337+ class NeedsManualVerification (
338+ val verificationId : String ,
339+ val token : PhoneAuthProvider .ForceResendingToken ,
340+ ) : VerifyPhoneNumberResult()
341+ }
342+
309343 internal fun validate () {
310344 defaultNumber?.let {
311345 check(PhoneNumberUtils .isValid(it)) {
@@ -329,6 +363,71 @@ abstract class AuthProvider(open val providerId: String) {
329363 }
330364 }
331365 }
366+
367+ /* *
368+ * Internal coroutine-based wrapper for Firebase Phone Authentication verification.
369+ *
370+ * This method wraps the callback-based Firebase Phone Auth API into a suspending function
371+ * using Kotlin coroutines. It handles the Firebase [PhoneAuthProvider.OnVerificationStateChangedCallbacks]
372+ * and converts them into a [VerifyPhoneNumberResult].
373+ *
374+ * **Callback mapping:**
375+ * - `onVerificationCompleted` → [VerifyPhoneNumberResult.AutoVerified]
376+ * - `onCodeSent` → [VerifyPhoneNumberResult.NeedsManualVerification]
377+ * - `onVerificationFailed` → throws the exception
378+ *
379+ * This is a private helper method used by [verifyPhoneNumber]. Callers should use
380+ * [verifyPhoneNumber] instead as it handles state management and error handling.
381+ *
382+ * @param auth The [FirebaseAuth] instance to use for verification
383+ * @param phoneNumber The phone number to verify in E.164 format
384+ * @param multiFactorSession Optional [MultiFactorSession] for MFA enrollment. When provided,
385+ * Firebase verifies the phone number for enrolling as a second authentication factor
386+ * instead of primary sign-in. Pass null for standard phone authentication.
387+ * @param forceResendingToken Optional token from previous verification for resending
388+ *
389+ * @return [VerifyPhoneNumberResult] indicating auto-verified or manual verification needed
390+ * @throws FirebaseException if verification fails
391+ */
392+ internal suspend fun verifyPhoneNumberAwait (
393+ auth : FirebaseAuth ,
394+ phoneNumber : String ,
395+ multiFactorSession : MultiFactorSession ? = null,
396+ forceResendingToken : PhoneAuthProvider .ForceResendingToken ? ,
397+ ): VerifyPhoneNumberResult = suspendCoroutine { continuation ->
398+ val options = PhoneAuthOptions .newBuilder(auth)
399+ .setPhoneNumber(phoneNumber)
400+ .requireSmsValidation(! isInstantVerificationEnabled)
401+ .setTimeout(timeout, TimeUnit .SECONDS )
402+ .setCallbacks(object : PhoneAuthProvider .OnVerificationStateChangedCallbacks () {
403+ override fun onVerificationCompleted (credential : PhoneAuthCredential ) {
404+ continuation.resume(VerifyPhoneNumberResult .AutoVerified (credential))
405+ }
406+
407+ override fun onVerificationFailed (e : FirebaseException ) {
408+ continuation.resumeWithException(e)
409+ }
410+
411+ override fun onCodeSent (
412+ verificationId : String ,
413+ token : PhoneAuthProvider .ForceResendingToken ,
414+ ) {
415+ continuation.resume(
416+ VerifyPhoneNumberResult .NeedsManualVerification (
417+ verificationId,
418+ token
419+ )
420+ )
421+ }
422+ })
423+ if (forceResendingToken != null ) {
424+ options.setForceResendingToken(forceResendingToken)
425+ }
426+ if (multiFactorSession != null ) {
427+ options.setMultiFactorSession(multiFactorSession)
428+ }
429+ PhoneAuthProvider .verifyPhoneNumber(options.build())
430+ }
332431 }
333432
334433 /* *
0 commit comments