Skip to content

Commit b14c136

Browse files
authored
feat: PhoneAuthScreen (#2243)
* feat: added libphonenumber for phone number validation * feat: PhoneAuthScreen, enter phone number and verification code * wip: e2e tests for PhoneAuthScreen * fix failing tests * chore: localize strings * chore: firebase auth image (temp) * refactor: remove user defined instant verification flag from UI * fix: bump robolectric to 4.15.1 required because dismissing a ModalBottomSheet failed in compose ui tests * test: e2e tests for PhoneAuthScreen * chore: localize strings * fix: resend code timer timing issues * fix: resend code timer timing issues
1 parent c3f2f80 commit b14c136

File tree

122 files changed

+2835
-302
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+2835
-302
lines changed

auth/build.gradle.kts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import com.android.build.gradle.internal.dsl.TestOptions
2-
31
plugins {
42
id("com.android.library")
53
id("com.vanniktech.maven.publish")
@@ -13,7 +11,7 @@ android {
1311

1412
defaultConfig {
1513
minSdk = Config.SdkVersions.min
16-
targetSdk =Config.SdkVersions.target
14+
targetSdk = Config.SdkVersions.target
1715

1816
buildConfigField("String", "VERSION_NAME", "\"${Config.version}\"")
1917

@@ -27,8 +25,8 @@ android {
2725
consumerProguardFiles("auth-proguard.pro")
2826
}
2927
}
30-
31-
compileOptions {
28+
29+
compileOptions {
3230
sourceCompatibility = JavaVersion.VERSION_17
3331
targetCompatibility = JavaVersion.VERSION_17
3432
}
@@ -82,8 +80,8 @@ dependencies {
8280
implementation(Config.Libs.Androidx.Compose.tooling)
8381
implementation(Config.Libs.Androidx.Compose.toolingPreview)
8482
implementation(Config.Libs.Androidx.Compose.activityCompose)
85-
implementation(Config.Libs.Androidx.materialDesign)
8683
implementation(Config.Libs.Androidx.activity)
84+
implementation(Config.Libs.Androidx.materialDesign)
8785
implementation(Config.Libs.Androidx.Compose.materialIconsExtended)
8886
implementation(Config.Libs.Androidx.datastorePreferences)
8987
// The new activity result APIs force us to include Fragment 1.3.0
@@ -106,6 +104,9 @@ dependencies {
106104
api(Config.Libs.Firebase.auth)
107105
api(Config.Libs.PlayServices.auth)
108106

107+
// Phone number validation
108+
implementation(Config.Libs.Misc.libphonenumber)
109+
109110
compileOnly(Config.Libs.Provider.facebook)
110111
implementation(Config.Libs.Androidx.legacySupportv4) // Needed to override deps
111112
implementation(Config.Libs.Androidx.cardView) // Needed to override Facebook

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.firebase.ui.auth.compose.configuration.auth_provider
1616

17+
import android.app.Activity
1718
import android.content.Context
1819
import android.net.Uri
1920
import android.util.Log
@@ -44,6 +45,7 @@ import com.google.firebase.auth.TwitterAuthProvider
4445
import com.google.firebase.auth.UserProfileChangeRequest
4546
import com.google.firebase.auth.actionCodeSettings
4647
import kotlinx.coroutines.tasks.await
48+
import kotlinx.serialization.Serializable
4749
import java.util.concurrent.TimeUnit
4850
import kotlin.coroutines.resume
4951
import kotlin.coroutines.resumeWithException
@@ -404,13 +406,15 @@ abstract class AuthProvider(open val providerId: String) {
404406
*/
405407
internal suspend fun verifyPhoneNumberAwait(
406408
auth: FirebaseAuth,
409+
activity: Activity?,
407410
phoneNumber: String,
408411
multiFactorSession: MultiFactorSession? = null,
409412
forceResendingToken: PhoneAuthProvider.ForceResendingToken?,
410413
verifier: Verifier = DefaultVerifier(),
411414
): VerifyPhoneNumberResult {
412415
return verifier.verifyPhoneNumber(
413416
auth,
417+
activity,
414418
phoneNumber,
415419
timeout,
416420
forceResendingToken,
@@ -425,11 +429,12 @@ abstract class AuthProvider(open val providerId: String) {
425429
internal interface Verifier {
426430
suspend fun verifyPhoneNumber(
427431
auth: FirebaseAuth,
432+
activity: Activity?,
428433
phoneNumber: String,
429434
timeout: Long,
430435
forceResendingToken: PhoneAuthProvider.ForceResendingToken?,
431436
multiFactorSession: MultiFactorSession?,
432-
isInstantVerificationEnabled: Boolean
437+
isInstantVerificationEnabled: Boolean,
433438
): VerifyPhoneNumberResult
434439
}
435440

@@ -439,18 +444,20 @@ abstract class AuthProvider(open val providerId: String) {
439444
internal class DefaultVerifier : Verifier {
440445
override suspend fun verifyPhoneNumber(
441446
auth: FirebaseAuth,
447+
activity: Activity?,
442448
phoneNumber: String,
443449
timeout: Long,
444450
forceResendingToken: PhoneAuthProvider.ForceResendingToken?,
445451
multiFactorSession: MultiFactorSession?,
446-
isInstantVerificationEnabled: Boolean
452+
isInstantVerificationEnabled: Boolean,
447453
): VerifyPhoneNumberResult {
448454
return suspendCoroutine { continuation ->
449455
val options = PhoneAuthOptions.newBuilder(auth)
450456
.setPhoneNumber(phoneNumber)
451457
.requireSmsValidation(!isInstantVerificationEnabled)
452458
.setTimeout(timeout, TimeUnit.SECONDS)
453-
.setCallbacks(object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
459+
.setCallbacks(object :
460+
PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
454461
override fun onVerificationCompleted(credential: PhoneAuthCredential) {
455462
continuation.resume(VerifyPhoneNumberResult.AutoVerified(credential))
456463
}
@@ -471,11 +478,14 @@ abstract class AuthProvider(open val providerId: String) {
471478
)
472479
}
473480
})
474-
if (forceResendingToken != null) {
475-
options.setForceResendingToken(forceResendingToken)
481+
activity?.let {
482+
options.setActivity(it)
476483
}
477-
if (multiFactorSession != null) {
478-
options.setMultiFactorSession(multiFactorSession)
484+
forceResendingToken?.let {
485+
options.setForceResendingToken(it)
486+
}
487+
multiFactorSession?.let {
488+
options.setMultiFactorSession(it)
479489
}
480490
PhoneAuthProvider.verifyPhoneNumber(options.build())
481491
}
@@ -495,7 +505,10 @@ abstract class AuthProvider(open val providerId: String) {
495505
* @suppress
496506
*/
497507
internal class DefaultCredentialProvider : CredentialProvider {
498-
override fun getCredential(verificationId: String, smsCode: String): PhoneAuthCredential {
508+
override fun getCredential(
509+
verificationId: String,
510+
smsCode: String,
511+
): PhoneAuthCredential {
499512
return PhoneAuthProvider.getCredential(verificationId, smsCode)
500513
}
501514
}

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.firebase.ui.auth.compose.configuration.auth_provider
22

3+
import android.app.Activity
34
import com.firebase.ui.auth.compose.AuthException
45
import com.firebase.ui.auth.compose.AuthState
56
import com.firebase.ui.auth.compose.FirebaseAuthUI
@@ -101,6 +102,7 @@ import kotlinx.coroutines.CancellationException
101102
*/
102103
internal suspend fun FirebaseAuthUI.verifyPhoneNumber(
103104
provider: AuthProvider.Phone,
105+
activity: Activity?,
104106
phoneNumber: String,
105107
multiFactorSession: MultiFactorSession? = null,
106108
forceResendingToken: PhoneAuthProvider.ForceResendingToken? = null,
@@ -110,6 +112,7 @@ internal suspend fun FirebaseAuthUI.verifyPhoneNumber(
110112
updateAuthState(AuthState.Loading("Verifying phone number..."))
111113
val result = provider.verifyPhoneNumberAwait(
112114
auth = auth,
115+
activity = activity,
113116
phoneNumber = phoneNumber,
114117
multiFactorSession = multiFactorSession,
115118
forceResendingToken = forceResendingToken,

auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProvider.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ interface AuthUIStringProvider {
159159
/** Invalid phone number error */
160160
val invalidPhoneNumber: String
161161

162+
/** Missing phone number error */
163+
val missingPhoneNumber: String
164+
162165
/** Phone verification code entry form title */
163166
val enterConfirmationCode: String
164167

@@ -171,6 +174,9 @@ interface AuthUIStringProvider {
171174
/** Resend code link text */
172175
val resendCode: String
173176

177+
/** Resend code with timer */
178+
fun resendCodeTimer(timeFormatted: String): String
179+
174180
/** Verifying progress text */
175181
val verifying: String
176182

@@ -180,6 +186,36 @@ interface AuthUIStringProvider {
180186
/** SMS terms of service warning */
181187
val smsTermsOfService: String
182188

189+
/** Enter phone number title */
190+
val enterPhoneNumberTitle: String
191+
192+
/** Phone number hint */
193+
val phoneNumberHint: String
194+
195+
/** Send verification code button text */
196+
val sendVerificationCode: String
197+
198+
/** Enter verification code title with phone number */
199+
fun enterVerificationCodeTitle(phoneNumber: String): String
200+
201+
/** Verification code hint */
202+
val verificationCodeHint: String
203+
204+
/** Change phone number link text */
205+
val changePhoneNumber: String
206+
207+
/** Missing verification code error */
208+
val missingVerificationCode: String
209+
210+
/** Invalid verification code error */
211+
val invalidVerificationCode: String
212+
213+
/** Select country modal sheet title */
214+
val countrySelectorModalTitle: String
215+
216+
/** Select country modal sheet input field hint */
217+
val searchCountriesHint: String
218+
183219
// Provider Picker Strings
184220
/** Common button text for sign in */
185221
val signInDefault: String

auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/DefaultAuthUIStringProvider.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ class DefaultAuthUIStringProvider(
159159
get() = localizedContext.getString(R.string.fui_country_hint)
160160
override val invalidPhoneNumber: String
161161
get() = localizedContext.getString(R.string.fui_invalid_phone_number)
162+
override val missingPhoneNumber: String
163+
get() = localizedContext.getString(R.string.fui_required_field)
162164
override val enterConfirmationCode: String
163165
get() = localizedContext.getString(R.string.fui_enter_confirmation_code)
164166
override val verifyPhoneNumber: String
@@ -167,13 +169,47 @@ class DefaultAuthUIStringProvider(
167169
get() = localizedContext.getString(R.string.fui_resend_code_in)
168170
override val resendCode: String
169171
get() = localizedContext.getString(R.string.fui_resend_code)
172+
173+
override fun resendCodeTimer(timeFormatted: String): String =
174+
localizedContext.getString(R.string.fui_resend_code_in, timeFormatted)
175+
170176
override val verifying: String
171177
get() = localizedContext.getString(R.string.fui_verifying)
172178
override val incorrectCodeDialogBody: String
173179
get() = localizedContext.getString(R.string.fui_incorrect_code_dialog_body)
174180
override val smsTermsOfService: String
175181
get() = localizedContext.getString(R.string.fui_sms_terms_of_service)
176182

183+
override val enterPhoneNumberTitle: String
184+
get() = localizedContext.getString(R.string.fui_verify_phone_number_title)
185+
186+
override val phoneNumberHint: String
187+
get() = localizedContext.getString(R.string.fui_phone_hint)
188+
189+
override val sendVerificationCode: String
190+
get() = localizedContext.getString(R.string.fui_next_default)
191+
192+
override fun enterVerificationCodeTitle(phoneNumber: String): String =
193+
localizedContext.getString(R.string.fui_enter_confirmation_code) + " " + phoneNumber
194+
195+
override val verificationCodeHint: String
196+
get() = localizedContext.getString(R.string.fui_enter_confirmation_code)
197+
198+
override val changePhoneNumber: String
199+
get() = localizedContext.getString(R.string.fui_change_phone_number)
200+
201+
override val missingVerificationCode: String
202+
get() = localizedContext.getString(R.string.fui_required_field)
203+
204+
override val invalidVerificationCode: String
205+
get() = localizedContext.getString(R.string.fui_incorrect_code_dialog_body)
206+
207+
override val countrySelectorModalTitle: String
208+
get() = localizedContext.getString(R.string.fui_country_selector_title)
209+
210+
override val searchCountriesHint: String
211+
get() = localizedContext.getString(R.string.fui_search_country_field_hint)
212+
177213
/**
178214
* Multi-Factor Authentication Strings
179215
*/
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.configuration.validators
16+
17+
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
18+
import com.firebase.ui.auth.compose.data.CountryData
19+
import com.google.i18n.phonenumbers.NumberParseException
20+
import com.google.i18n.phonenumbers.PhoneNumberUtil
21+
22+
internal class PhoneNumberValidator(
23+
override val stringProvider: AuthUIStringProvider,
24+
val selectedCountry: CountryData,
25+
) :
26+
FieldValidator {
27+
private var _validationStatus = FieldValidationStatus(hasError = false, errorMessage = null)
28+
private val phoneNumberUtil = PhoneNumberUtil.getInstance()
29+
30+
override val hasError: Boolean
31+
get() = _validationStatus.hasError
32+
33+
override val errorMessage: String
34+
get() = _validationStatus.errorMessage ?: ""
35+
36+
override fun validate(value: String): Boolean {
37+
if (value.isEmpty()) {
38+
_validationStatus = FieldValidationStatus(
39+
hasError = true,
40+
errorMessage = stringProvider.missingPhoneNumber
41+
)
42+
return false
43+
}
44+
45+
try {
46+
val phoneNumber = phoneNumberUtil.parse(value, selectedCountry.countryCode)
47+
val isValid = phoneNumberUtil.isValidNumber(phoneNumber)
48+
49+
if (!isValid) {
50+
_validationStatus = FieldValidationStatus(
51+
hasError = true,
52+
errorMessage = stringProvider.invalidPhoneNumber
53+
)
54+
return false
55+
}
56+
} catch (_: NumberParseException) {
57+
_validationStatus = FieldValidationStatus(
58+
hasError = true,
59+
errorMessage = stringProvider.invalidPhoneNumber
60+
)
61+
return false
62+
}
63+
64+
_validationStatus = FieldValidationStatus(hasError = false, errorMessage = null)
65+
return true
66+
}
67+
}

0 commit comments

Comments
 (0)