Skip to content

Commit e7b752e

Browse files
committed
feat: P3 — Phone Provider Integration
1 parent d2becd6 commit e7b752e

File tree

3 files changed

+481
-3
lines changed

3 files changed

+481
-3
lines changed

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.google.firebase.auth.AuthCredential
2020
import com.google.firebase.auth.AuthResult
2121
import com.google.firebase.auth.FirebaseUser
2222
import com.google.firebase.auth.MultiFactorResolver
23+
import com.google.firebase.auth.PhoneAuthCredential
24+
import com.google.firebase.auth.PhoneAuthProvider
2325

2426
/**
2527
* Represents the authentication state in Firebase Auth UI.
@@ -252,6 +254,74 @@ abstract class AuthState private constructor() {
252254
override fun toString(): String = "AuthState.EmailSignInLinkSent"
253255
}
254256

257+
/**
258+
* Phone number was automatically verified via SMS instant verification.
259+
*
260+
* This state is emitted when Firebase Phone Authentication successfully retrieves
261+
* and verifies the SMS code automatically without user interaction. This happens
262+
* when Google Play services can detect the incoming SMS message.
263+
*
264+
* @property credential The [PhoneAuthCredential] that can be used to sign in the user
265+
*
266+
* @see PhoneNumberVerificationRequired for the manual verification flow
267+
*/
268+
class SMSAutoVerified(val credential: PhoneAuthCredential) : AuthState() {
269+
override fun equals(other: Any?): Boolean {
270+
if (this === other) return true
271+
if (other !is SMSAutoVerified) return false
272+
return credential == other.credential
273+
}
274+
275+
override fun hashCode(): Int {
276+
var result = credential.hashCode()
277+
result = 31 * result + credential.hashCode()
278+
return result
279+
}
280+
281+
override fun toString(): String =
282+
"AuthState.SMSAutoVerified(credential=$credential)"
283+
}
284+
285+
/**
286+
* Phone number verification requires manual code entry.
287+
*
288+
* This state is emitted when Firebase Phone Authentication cannot instantly verify
289+
* the phone number and sends an SMS code that the user must manually enter. This is
290+
* the normal flow when automatic SMS retrieval is not available or fails.
291+
*
292+
* **Resending codes:**
293+
* To allow users to resend the verification code (if they didn't receive it),
294+
* call [FirebaseAuthUI.verifyPhoneNumber] again with:
295+
* - `isForceResendingTokenEnabled = true`
296+
* - `forceResendingToken` from this state
297+
*
298+
* @property verificationId The verification ID to use when submitting the code.
299+
* This must be passed to [FirebaseAuthUI.submitVerificationCode].
300+
* @property forceResendingToken Token that can be used to resend the SMS code if needed
301+
*
302+
*/
303+
class PhoneNumberVerificationRequired(
304+
val verificationId: String,
305+
val forceResendingToken: PhoneAuthProvider.ForceResendingToken,
306+
) : AuthState() {
307+
override fun equals(other: Any?): Boolean {
308+
if (this === other) return true
309+
if (other !is PhoneNumberVerificationRequired) return false
310+
return verificationId == other.verificationId &&
311+
forceResendingToken == other.forceResendingToken
312+
}
313+
314+
override fun hashCode(): Int {
315+
var result = verificationId.hashCode()
316+
result = 31 * result + forceResendingToken.hashCode()
317+
return result
318+
}
319+
320+
override fun toString(): String =
321+
"AuthState.PhoneNumberVerificationRequired(verificationId=$verificationId, " +
322+
"forceResendingToken=$forceResendingToken)"
323+
}
324+
255325
companion object {
256326
/**
257327
* Creates an Idle state instance.

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,25 @@ import com.firebase.ui.auth.util.Preconditions
2828
import com.firebase.ui.auth.util.data.ContinueUrlBuilder
2929
import com.firebase.ui.auth.util.data.PhoneNumberUtils
3030
import com.firebase.ui.auth.util.data.ProviderAvailability
31+
import com.google.firebase.FirebaseException
3132
import com.google.firebase.auth.ActionCodeSettings
3233
import com.google.firebase.auth.EmailAuthProvider
3334
import com.google.firebase.auth.FacebookAuthProvider
3435
import com.google.firebase.auth.FirebaseAuth
3536
import com.google.firebase.auth.GithubAuthProvider
3637
import 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
3741
import com.google.firebase.auth.PhoneAuthProvider
3842
import com.google.firebase.auth.TwitterAuthProvider
3943
import com.google.firebase.auth.UserProfileChangeRequest
4044
import com.google.firebase.auth.actionCodeSettings
4145
import 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
4452
class 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

Comments
 (0)