Skip to content
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
9af10d2
feat: AuthMethodPicker, logo and provider theme style
demolaf Sep 29, 2025
b6f7bdc
chore: organize folder structure
demolaf Sep 30, 2025
96ffa30
feat: TOS and PP footer, ui tests for AuthMethodPicker
demolaf Sep 30, 2025
bfd056c
chore: tests folder structure
demolaf Sep 30, 2025
4c3a59b
chore: use version catalog for compose deps
demolaf Sep 30, 2025
a840716
feat: AuthTextField with validation
demolaf Sep 30, 2025
58145d6
test: AuthTextField and field validations
demolaf Oct 1, 2025
24c687c
chore: update doc comments
demolaf Oct 1, 2025
76923bc
wip: Email Provider integration
demolaf Oct 2, 2025
7f73bcf
chore: upgrade mockito, fix: spying mocked objects in new library
demolaf Oct 2, 2025
70cd57a
wip: Email provider integration
demolaf Oct 2, 2025
42ebf15
wip: Email provider integration
demolaf Oct 4, 2025
b1d69e7
wip: Email provider integration
demolaf Oct 6, 2025
751fcc9
feat: Email provider integration
demolaf Oct 6, 2025
9fc948d
Merge branch 'version-10.0.0-dev' of https://github.com/firebase/Fire…
demolaf Oct 6, 2025
5bd8c7c
Merge branch 'version-10.0.0-dev' of https://github.com/firebase/Fire…
demolaf Oct 6, 2025
32637b4
wip: SignIn, SignUp, ResetPassword flows
demolaf Oct 7, 2025
eb38a85
refactor: remove libs.versions.toml catalog file
demolaf Oct 7, 2025
45b6a40
Merge branch 'version-10.0.0-dev' of https://github.com/firebase/Fire…
demolaf Oct 7, 2025
3098775
add sample app compose module
demolaf Oct 7, 2025
0e97d3f
Merge branch 'feat/T3' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 7, 2025
2d6fa27
Merge branch 'feat/T3' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 7, 2025
ee2d922
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 7, 2025
b30f379
wip: SignInUI and EmailAuthScreen sample
demolaf Oct 7, 2025
ad1392d
feat: Email provider integration
demolaf Oct 7, 2025
76af4b9
Merge branch 'version-10.0.0-dev' of https://github.com/firebase/Fire…
demolaf Oct 7, 2025
9bac9a5
wip: SignUp UI
demolaf Oct 7, 2025
84b914a
feat: add PasswordResetLinkSent state
demolaf Oct 7, 2025
ca7d009
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 7, 2025
9a1520f
fix: use isSecureTextField for password fields
demolaf Oct 8, 2025
6e527f1
wip: SignUp
demolaf Oct 8, 2025
0d97227
fix: passwordResetActionCodeSettings for send password reset link
demolaf Oct 8, 2025
4e09672
fix: combine Firebase and internal auth state flows to prioritize non…
demolaf Oct 8, 2025
ff17661
wip: SignUp
demolaf Oct 8, 2025
9c665ca
chore: remove unused methods
demolaf Oct 8, 2025
6351838
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 8, 2025
e31ffec
chore: remove unused comments and code
demolaf Oct 8, 2025
01e873f
chore: remove unused imports, reformat
demolaf Oct 8, 2025
d4b0fbb
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 8, 2025
f8a3014
chore: remove comments
demolaf Oct 8, 2025
edbf241
chore: remove comments
demolaf Oct 8, 2025
ca4a5fb
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 8, 2025
aeec7dc
handle authState exceptions
demolaf Oct 8, 2025
16c6126
fix: mockito 5 upgrade stubbing issues
demolaf Oct 8, 2025
9fe6660
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into …
demolaf Oct 8, 2025
21759b4
wip: Email link, deep link
demolaf Oct 8, 2025
a6337b6
Merge branch 'version-10.0.0-dev' of https://github.com/firebase/Fire…
demolaf Oct 8, 2025
92cc5dd
chore: add copyright message
demolaf Oct 8, 2025
a467dde
refactor: rename to emailLinkActionCodeSettings in AuthProvider.Email…
demolaf Oct 8, 2025
50dddb3
feat: add dark theme
demolaf Oct 8, 2025
23c1234
feat: Email sign in link
demolaf Oct 9, 2025
2bb25a4
fix: test doesn't capture initial Idle state
demolaf Oct 9, 2025
cc63ca0
fix: CI run issues
demolaf Oct 9, 2025
513fa3d
fix: CI run issues
demolaf Oct 9, 2025
b04afcb
fix: opt out of edge to edge in app module
demolaf Oct 9, 2025
c7438a9
fix: remove opt out of edge to edge in app module
demolaf Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/src/main/res/values-v35/styles.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryVariant</item>
<item name="colorPrimaryVariant">@color/colorPrimaryVariant</item>
<item name="colorSecondary">@color/colorSecondary</item>
<!-- Opt-out of edge-to-edge enforcement for Android 15 (API 35) -->
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
12 changes: 11 additions & 1 deletion auth/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@

<activity
android:name=".ui.email.EmailLinkCatcherActivity"
android:exported="false"
android:label=""
android:exported="false"
android:theme="@style/FirebaseUI.Transparent"
android:windowSoftInputMode="adjustResize" />

Expand Down Expand Up @@ -119,6 +119,16 @@
</intent-filter>
</activity>

<!-- Email Link Sign-In Handler Activity for Compose -->
<!-- IMPORTANT: This activity is NOT exported by default -->
<!-- Users must declare this activity with an intent filter in their app's AndroidManifest.xml -->
<!-- See documentation for setup instructions -->
<activity
android:name=".compose.ui.screens.EmailSignInLinkHandlerActivity"
android:label=""
android:exported="false"
android:theme="@style/FirebaseUI.Transparent" />

<provider
android:name=".data.client.AuthUiInitProvider"
android:authorities="${applicationId}.authuiinitprovider"
Expand Down
9 changes: 9 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/compose/AuthState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,15 @@ abstract class AuthState private constructor() {
override fun toString(): String = "AuthState.PasswordResetLinkSent"
}

/**
* Email sign in link has been sent to the user's email.
*/
class EmailSignInLinkSent : AuthState() {
override fun equals(other: Any?): Boolean = other is EmailSignInLinkSent
override fun hashCode(): Int = javaClass.hashCode()
override fun toString(): String = "AuthState.EmailSignInLinkSent"
}

companion object {
/**
* Creates an Idle state instance.
Expand Down
84 changes: 47 additions & 37 deletions auth/src/main/java/com/firebase/ui/auth/compose/FirebaseAuthUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.tasks.await
import java.util.concurrent.ConcurrentHashMap

Expand Down Expand Up @@ -153,53 +158,58 @@ class FirebaseAuthUI private constructor(
*
* @return A [Flow] of [AuthState] that emits authentication state changes
*/
fun authStateFlow(): Flow<AuthState> = callbackFlow {
// Set initial state based on current auth state
val initialState = auth.currentUser?.let { user ->
AuthState.Success(result = null, user = user, isNewUser = false)
} ?: AuthState.Idle
fun authStateFlow(): Flow<AuthState> {
// Create a flow from FirebaseAuth state listener
val firebaseAuthFlow = callbackFlow {
// Set initial state based on current auth state
val initialState = auth.currentUser?.let { user ->
AuthState.Success(result = null, user = user, isNewUser = false)
} ?: AuthState.Idle

trySend(initialState)
trySend(initialState)

// Create auth state listener
val authStateListener = AuthStateListener { firebaseAuth ->
val currentUser = firebaseAuth.currentUser
val state = if (currentUser != null) {
// Check if email verification is required
if (!currentUser.isEmailVerified &&
currentUser.email != null &&
currentUser.providerData.any { it.providerId == "password" }
) {
AuthState.RequiresEmailVerification(
user = currentUser,
email = currentUser.email!!
)
// Create auth state listener
val authStateListener = AuthStateListener { firebaseAuth ->
val currentUser = firebaseAuth.currentUser
val state = if (currentUser != null) {
// Check if email verification is required
if (!currentUser.isEmailVerified &&
currentUser.email != null &&
currentUser.providerData.any { it.providerId == "password" }
) {
AuthState.RequiresEmailVerification(
user = currentUser,
email = currentUser.email!!
)
} else {
AuthState.Success(
result = null,
user = currentUser,
isNewUser = false
)
}
} else {
AuthState.Success(
result = null,
user = currentUser,
isNewUser = false
)
AuthState.Idle
}
} else {
AuthState.Idle
trySend(state)
}
trySend(state)
}

// Add listener
auth.addAuthStateListener(authStateListener)
// Add listener
auth.addAuthStateListener(authStateListener)

// Also observe internal state changes
_authStateFlow.value.let { currentState ->
if (currentState !is AuthState.Idle && currentState !is AuthState.Success) {
trySend(currentState)
// Remove listener when flow collection is cancelled
awaitClose {
auth.removeAuthStateListener(authStateListener)
}
}

// Remove listener when flow collection is cancelled
awaitClose {
auth.removeAuthStateListener(authStateListener)
// Also observe internal state changes
return combine(
firebaseAuthFlow,
_authStateFlow
) { firebaseState, internalState ->
// Prefer non-idle internal states (like PasswordResetLinkSent, Error, etc.)
if (internalState !is AuthState.Idle) internalState else firebaseState
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class AuthUIConfigurationBuilder {
var tosUrl: String? = null
var privacyPolicyUrl: String? = null
var logo: ImageVector? = null
var actionCodeSettings: ActionCodeSettings? = null
var passwordResetActionCodeSettings: ActionCodeSettings? = null
var isNewEmailAccountsAllowed: Boolean = true
var isDisplayNameRequired: Boolean = true
var isProviderChoiceAlwaysShown: Boolean = false
Expand Down Expand Up @@ -85,17 +85,7 @@ class AuthUIConfigurationBuilder {
// Provider specific validations
providers.forEach { provider ->
when (provider) {
is AuthProvider.Email -> {
provider.validate()

if (isAnonymousUpgradeEnabled && provider.isEmailLinkSignInEnabled) {
check(provider.isEmailLinkForceSameDeviceEnabled) {
"You must force the same device flow when using email link sign in " +
"with anonymous user upgrade"
}
}
}

is AuthProvider.Email -> provider.validate(isAnonymousUpgradeEnabled)
is AuthProvider.Phone -> provider.validate()
is AuthProvider.Google -> provider.validate(context)
is AuthProvider.Facebook -> provider.validate(context)
Expand All @@ -116,7 +106,7 @@ class AuthUIConfigurationBuilder {
tosUrl = tosUrl,
privacyPolicyUrl = privacyPolicyUrl,
logo = logo,
actionCodeSettings = actionCodeSettings,
passwordResetActionCodeSettings = passwordResetActionCodeSettings,
isNewEmailAccountsAllowed = isNewEmailAccountsAllowed,
isDisplayNameRequired = isDisplayNameRequired,
isProviderChoiceAlwaysShown = isProviderChoiceAlwaysShown
Expand Down Expand Up @@ -184,9 +174,9 @@ class AuthUIConfiguration(
val logo: ImageVector? = null,

/**
* Configuration for email link sign-in.
* Configuration for sending email reset link.
*/
val actionCodeSettings: ActionCodeSettings? = null,
val passwordResetActionCodeSettings: ActionCodeSettings? = null,

/**
* Allows new email accounts to be created. Defaults to true.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ abstract class AuthProvider(open val providerId: String) {
/**
* Settings for email link actions.
*/
val actionCodeSettings: ActionCodeSettings?,
val emailLinkActionCodeSettings: ActionCodeSettings?,

/**
* Allows new accounts to be created. Defaults to true.
Expand Down Expand Up @@ -202,9 +202,11 @@ abstract class AuthProvider(open val providerId: String) {
val KEY_IDP_SECRET = stringPreferencesKey("com.firebase.ui.auth.data.client.idpSecret")
}

internal fun validate() {
internal fun validate(
isAnonymousUpgradeEnabled: Boolean = false
) {
if (isEmailLinkSignInEnabled) {
val actionCodeSettings = requireNotNull(actionCodeSettings) {
val actionCodeSettings = requireNotNull(emailLinkActionCodeSettings) {
"ActionCodeSettings cannot be null when using " +
"email link sign in."
}
Expand All @@ -213,6 +215,13 @@ abstract class AuthProvider(open val providerId: String) {
"You must set canHandleCodeInApp in your " +
"ActionCodeSettings to true for Email-Link Sign-in."
}

if (isAnonymousUpgradeEnabled) {
check(isEmailLinkForceSameDeviceEnabled) {
"You must force the same device flow when using email link sign in " +
"with anonymous user upgrade"
}
}
}
}

Expand All @@ -221,11 +230,11 @@ abstract class AuthProvider(open val providerId: String) {
sessionId: String,
anonymousUserId: String,
): ActionCodeSettings {
requireNotNull(actionCodeSettings) {
requireNotNull(emailLinkActionCodeSettings) {
"ActionCodeSettings is required for email link sign in"
}

val continueUrl = continueUrl(actionCodeSettings.url) {
val continueUrl = continueUrl(emailLinkActionCodeSettings.url) {
appendSessionId(sessionId)
appendAnonymousUserId(anonymousUserId)
appendForceSameDeviceBit(isEmailLinkForceSameDeviceEnabled)
Expand All @@ -234,12 +243,12 @@ abstract class AuthProvider(open val providerId: String) {

return actionCodeSettings {
url = continueUrl
handleCodeInApp = actionCodeSettings.canHandleCodeInApp()
iosBundleId = actionCodeSettings.iosBundle
handleCodeInApp = emailLinkActionCodeSettings.canHandleCodeInApp()
iosBundleId = emailLinkActionCodeSettings.iosBundle
setAndroidPackageName(
actionCodeSettings.androidPackageName ?: "",
actionCodeSettings.androidInstallApp,
actionCodeSettings.androidMinimumVersion
emailLinkActionCodeSettings.androidPackageName ?: "",
emailLinkActionCodeSettings.androidInstallApp,
emailLinkActionCodeSettings.androidMinimumVersion
)
}
}
Expand Down
Loading