Skip to content

Commit a841656

Browse files
committed
feat: Email Provider Integration
1 parent 68df21d commit a841656

File tree

24 files changed

+3002
-87
lines changed

24 files changed

+3002
-87
lines changed

auth/build.gradle.kts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,24 @@ android {
7474
}
7575

7676
dependencies {
77-
implementation(platform(Config.Libs.Androidx.Compose.bom))
78-
implementation(Config.Libs.Androidx.Compose.ui)
79-
implementation(Config.Libs.Androidx.Compose.uiGraphics)
80-
implementation(Config.Libs.Androidx.Compose.material3)
81-
implementation(Config.Libs.Androidx.Compose.foundation)
82-
implementation(Config.Libs.Androidx.Compose.tooling)
83-
implementation(Config.Libs.Androidx.Compose.toolingPreview)
84-
implementation(Config.Libs.Androidx.Compose.activityCompose)
77+
implementation(platform(libs.androidx.compose.bom))
78+
implementation(libs.androidx.compose.ui)
79+
implementation(libs.androidx.compose.ui.graphics)
80+
implementation(libs.androidx.compose.material3)
81+
implementation(libs.androidx.compose.foundation)
82+
implementation(libs.androidx.compose.ui.tooling)
83+
implementation(libs.androidx.compose.ui.tooling.preview)
84+
implementation(libs.androidx.activity.compose)
8585
implementation(Config.Libs.Androidx.materialDesign)
8686
implementation(Config.Libs.Androidx.activity)
87+
implementation(libs.androidx.compose.material.icons.extended)
88+
implementation(libs.androidx.datastore.preferences)
8789
// The new activity result APIs force us to include Fragment 1.3.0
8890
// See https://issuetracker.google.com/issues/152554847
8991
implementation(Config.Libs.Androidx.fragment)
9092
implementation(Config.Libs.Androidx.customTabs)
9193
implementation(Config.Libs.Androidx.constraint)
92-
implementation("androidx.credentials:credentials:1.3.0")
94+
implementation(libs.androidx.credentials)
9395
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
9496

9597
implementation(Config.Libs.Androidx.lifecycleExtensions)
@@ -110,12 +112,27 @@ dependencies {
110112

111113
testImplementation(Config.Libs.Test.junit)
112114
testImplementation(Config.Libs.Test.truth)
113-
testImplementation(Config.Libs.Test.mockito)
114115
testImplementation(Config.Libs.Test.core)
115116
testImplementation(Config.Libs.Test.robolectric)
116117
testImplementation(Config.Libs.Test.kotlinReflect)
117118
testImplementation(Config.Libs.Provider.facebook)
118119
testImplementation(libs.androidx.ui.test.junit4)
120+
testImplementation(libs.mockito)
121+
testImplementation(libs.mockito.inline)
122+
testImplementation(libs.mockito.kotlin)
123+
testImplementation(libs.androidx.credentials)
119124

120125
debugImplementation(project(":internal:lintchecks"))
121126
}
127+
128+
val mockitoAgent by configurations.creating
129+
130+
dependencies {
131+
mockitoAgent(libs.mockito) {
132+
isTransitive = false
133+
}
134+
}
135+
136+
tasks.withType<Test>().configureEach {
137+
jvmArgs("-javaagent:${mockitoAgent.asPath}")
138+
}

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

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

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

17+
import com.firebase.ui.auth.compose.AuthException.Companion.from
1718
import com.google.firebase.FirebaseException
1819
import com.google.firebase.auth.FirebaseAuthException
1920
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
@@ -204,6 +205,38 @@ abstract class AuthException(
204205
cause: Throwable? = null
205206
) : AuthException(message, cause)
206207

208+
class InvalidEmailLinkException(
209+
cause: Throwable? = null
210+
) : AuthException("You are are attempting to sign in with an invalid email link", cause)
211+
212+
class EmailLinkWrongDeviceException(
213+
cause: Throwable? = null
214+
) : AuthException("You must open the email link on the same device.", cause)
215+
216+
class EmailLinkCrossDeviceLinkingException(
217+
cause: Throwable? = null
218+
) : AuthException(
219+
"You must determine if you want to continue linking or " +
220+
"complete the sign in", cause
221+
)
222+
223+
class EmailLinkPromptForEmailException(
224+
cause: Throwable? = null
225+
) : AuthException("Please enter your email to continue signing in", cause)
226+
227+
class EmailLinkDifferentAnonymousUserException(
228+
cause: Throwable? = null
229+
) : AuthException(
230+
"The session associated with this sign-in request has either expired or " +
231+
"was cleared", cause
232+
)
233+
234+
class EmailMismatchException(
235+
cause: Throwable? = null
236+
) : AuthException(
237+
"You are are attempting to sign in a different email than previously " +
238+
"provided", cause)
239+
207240
companion object {
208241
/**
209242
* Creates an appropriate [AuthException] instance from a Firebase authentication exception.
@@ -244,86 +277,111 @@ abstract class AuthException(
244277
cause = firebaseException
245278
)
246279
}
280+
247281
is FirebaseAuthInvalidUserException -> {
248282
when (firebaseException.errorCode) {
249283
"ERROR_USER_NOT_FOUND" -> UserNotFoundException(
250284
message = firebaseException.message ?: "User not found",
251285
cause = firebaseException
252286
)
287+
253288
"ERROR_USER_DISABLED" -> InvalidCredentialsException(
254289
message = firebaseException.message ?: "User account has been disabled",
255290
cause = firebaseException
256291
)
292+
257293
else -> UserNotFoundException(
258294
message = firebaseException.message ?: "User account error",
259295
cause = firebaseException
260296
)
261297
}
262298
}
299+
263300
is FirebaseAuthWeakPasswordException -> {
264301
WeakPasswordException(
265302
message = firebaseException.message ?: "Password is too weak",
266303
cause = firebaseException,
267304
reason = firebaseException.reason
268305
)
269306
}
307+
270308
is FirebaseAuthUserCollisionException -> {
271309
when (firebaseException.errorCode) {
272310
"ERROR_EMAIL_ALREADY_IN_USE" -> EmailAlreadyInUseException(
273-
message = firebaseException.message ?: "Email address is already in use",
311+
message = firebaseException.message
312+
?: "Email address is already in use",
274313
cause = firebaseException,
275314
email = firebaseException.email
276315
)
316+
277317
"ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL" -> AccountLinkingRequiredException(
278-
message = firebaseException.message ?: "Account already exists with different credentials",
318+
message = firebaseException.message
319+
?: "Account already exists with different credentials",
279320
cause = firebaseException
280321
)
322+
281323
"ERROR_CREDENTIAL_ALREADY_IN_USE" -> AccountLinkingRequiredException(
282-
message = firebaseException.message ?: "Credential is already associated with a different user account",
324+
message = firebaseException.message
325+
?: "Credential is already associated with a different user account",
283326
cause = firebaseException
284327
)
328+
285329
else -> AccountLinkingRequiredException(
286330
message = firebaseException.message ?: "Account collision error",
287331
cause = firebaseException
288332
)
289333
}
290334
}
335+
291336
is FirebaseAuthMultiFactorException -> {
292337
MfaRequiredException(
293-
message = firebaseException.message ?: "Multi-factor authentication required",
338+
message = firebaseException.message
339+
?: "Multi-factor authentication required",
294340
cause = firebaseException
295341
)
296342
}
343+
297344
is FirebaseAuthRecentLoginRequiredException -> {
298345
InvalidCredentialsException(
299-
message = firebaseException.message ?: "Recent login required for this operation",
346+
message = firebaseException.message
347+
?: "Recent login required for this operation",
300348
cause = firebaseException
301349
)
302350
}
351+
303352
is FirebaseAuthException -> {
304353
// Handle FirebaseAuthException and check for specific error codes
305354
when (firebaseException.errorCode) {
306355
"ERROR_TOO_MANY_REQUESTS" -> TooManyRequestsException(
307-
message = firebaseException.message ?: "Too many requests. Please try again later",
356+
message = firebaseException.message
357+
?: "Too many requests. Please try again later",
308358
cause = firebaseException
309359
)
360+
310361
else -> UnknownException(
311-
message = firebaseException.message ?: "An unknown authentication error occurred",
362+
message = firebaseException.message
363+
?: "An unknown authentication error occurred",
312364
cause = firebaseException
313365
)
314366
}
315367
}
368+
316369
is FirebaseException -> {
317370
// Handle general Firebase exceptions, which include network errors
318371
NetworkException(
319372
message = firebaseException.message ?: "Network error occurred",
320373
cause = firebaseException
321374
)
322375
}
376+
323377
else -> {
324378
// Check for common cancellation patterns
325-
if (firebaseException.message?.contains("cancelled", ignoreCase = true) == true ||
326-
firebaseException.message?.contains("canceled", ignoreCase = true) == true) {
379+
if (firebaseException.message?.contains(
380+
"cancelled",
381+
ignoreCase = true
382+
) == true ||
383+
firebaseException.message?.contains("canceled", ignoreCase = true) == true
384+
) {
327385
AuthCancelledException(
328386
message = firebaseException.message ?: "Authentication was cancelled",
329387
cause = firebaseException

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

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

17+
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
18+
import com.google.firebase.auth.AuthCredential
1719
import com.google.firebase.auth.AuthResult
1820
import com.google.firebase.auth.FirebaseUser
1921
import com.google.firebase.auth.MultiFactorResolver
@@ -204,6 +206,63 @@ abstract class AuthState private constructor() {
204206
"AuthState.RequiresProfileCompletion(user=$user, missingFields=$missingFields)"
205207
}
206208

209+
/**
210+
* The user needs to sign in with a different provider.
211+
*
212+
* Emitted when a user tries to sign up with an email that already exists
213+
* and needs to use the existing provider to sign in instead.
214+
*
215+
* @property provider The [AuthProvider] the user should sign in with
216+
* @property email The email address of the existing account
217+
*/
218+
class RequiresSignIn(
219+
val provider: AuthProvider,
220+
val email: String
221+
) : AuthState() {
222+
override fun equals(other: Any?): Boolean {
223+
if (this === other) return true
224+
if (other !is RequiresSignIn) return false
225+
return provider == other.provider &&
226+
email == other.email
227+
}
228+
229+
override fun hashCode(): Int {
230+
var result = provider.hashCode()
231+
result = 31 * result + email.hashCode()
232+
return result
233+
}
234+
235+
override fun toString(): String =
236+
"AuthState.RequiresSignIn(provider=$provider, email=$email)"
237+
}
238+
239+
/**
240+
* Pending credential for an anonymous upgrade merge conflict.
241+
*
242+
* Emitted when an anonymous user attempts to convert to a permanent account but
243+
* Firebase detects that the target email already belongs to another user. The UI can
244+
* prompt the user to resolve the conflict by signing in with the existing account and
245+
* later linking the stored [pendingCredential].
246+
*/
247+
class MergeConflict(
248+
val pendingCredential: AuthCredential
249+
) : AuthState() {
250+
override fun equals(other: Any?): Boolean {
251+
if (this === other) return true
252+
if (other !is MergeConflict) return false
253+
return pendingCredential == other.pendingCredential
254+
}
255+
256+
override fun hashCode(): Int {
257+
var result = pendingCredential.hashCode()
258+
result = 31 * result + pendingCredential.hashCode()
259+
return result
260+
}
261+
262+
override fun toString(): String =
263+
"AuthState.MergeConflict(pendingCredential=$pendingCredential)"
264+
}
265+
207266
companion object {
208267
/**
209268
* Creates an Idle state instance.
@@ -219,4 +278,4 @@ abstract class AuthState private constructor() {
219278
@JvmStatic
220279
val Cancelled: Cancelled = Cancelled()
221280
}
222-
}
281+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ class FirebaseAuthUI private constructor(
168168
// Check if email verification is required
169169
if (!currentUser.isEmailVerified &&
170170
currentUser.email != null &&
171-
currentUser.providerData.any { it.providerId == "password" }) {
171+
currentUser.providerData.any { it.providerId == "password" }
172+
) {
172173
AuthState.RequiresEmailVerification(
173174
user = currentUser,
174175
email = currentUser.email!!
@@ -374,7 +375,7 @@ class FirebaseAuthUI private constructor(
374375
} catch (e: IllegalStateException) {
375376
throw IllegalStateException(
376377
"Default FirebaseApp is not initialized. " +
377-
"Make sure to call FirebaseApp.initializeApp(Context) first.",
378+
"Make sure to call FirebaseApp.initializeApp(Context) first.",
378379
e
379380
)
380381
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ import com.google.firebase.auth.ActionCodeSettings
2020
import androidx.compose.ui.graphics.vector.ImageVector
2121
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
2222
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
23+
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
24+
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvidersBuilder
25+
import com.firebase.ui.auth.compose.configuration.auth_provider.Provider
2326
import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme
2427

25-
fun actionCodeSettings(block: ActionCodeSettings.Builder.() -> Unit) =
26-
ActionCodeSettings.newBuilder().apply(block).build()
27-
2828
fun authUIConfiguration(block: AuthUIConfigurationBuilder.() -> Unit) =
2929
AuthUIConfigurationBuilder().apply(block).build()
3030

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringPr
1818

1919
/**
2020
* An abstract class representing a set of validation rules that can be applied to a password field,
21-
* typically within the [AuthProvider.Email] configuration.
21+
* typically within the [com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider.Email] configuration.
2222
*/
2323
abstract class PasswordRule {
2424
/**

0 commit comments

Comments
 (0)