Skip to content

Commit 0afcb24

Browse files
committed
fix: email link sign in support
1 parent 99a930c commit 0afcb24

File tree

7 files changed

+99
-62
lines changed

7 files changed

+99
-62
lines changed

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,6 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
154154
* A list of custom password validation rules.
155155
*/
156156
val passwordValidationRules: List<PasswordRule>,
157-
158-
/**
159-
* Optional custom button label to differentiate between multiple email providers.
160-
* If null, uses the default string from stringProvider. Defaults to null.
161-
*/
162-
val buttonLabel: String? = null,
163157
) : AuthProvider(providerId = Provider.EMAIL.id, providerName = Provider.EMAIL.providerName) {
164158
companion object {
165159
const val SESSION_ID_LENGTH = 10

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ class AuthUITheme(
144144
val topAppBarColors
145145
get() = TopAppBarDefaults.topAppBarColors(
146146
containerColor = MaterialTheme.colorScheme.primary,
147-
titleContentColor = MaterialTheme.colorScheme.onPrimary
147+
titleContentColor = MaterialTheme.colorScheme.onPrimary,
148+
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
148149
)
149150
}
150151
}

auth/src/main/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButton.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,6 @@ internal fun resolveProviderLabel(
153153
context: android.content.Context
154154
): String = when (provider) {
155155
is AuthProvider.GenericOAuth -> provider.buttonLabel
156-
is AuthProvider.Email -> {
157-
// Use custom button label if provided, otherwise use default
158-
provider.buttonLabel ?: stringProvider.signInWithEmail
159-
}
160156
is AuthProvider.Apple -> {
161157
// Use Apple-specific locale if provided, otherwise use default stringProvider
162158
if (provider.locale != null) {

auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/email/EmailAuthScreen.kt

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class EmailAuthContentState(
9595
val displayName: String,
9696
val onDisplayNameChange: (String) -> Unit,
9797
val onSignInClick: () -> Unit,
98+
val onSignInEmailLinkClick: () -> Unit,
9899
val onSignUpClick: () -> Unit,
99100
val onSendResetLinkClick: () -> Unit,
100101
val resetLinkSent: Boolean = false,
@@ -166,32 +167,26 @@ fun EmailAuthScreen(
166167
is AuthState.Error -> {
167168
val exception = AuthException.from(state.exception)
168169
onError(exception)
169-
170-
// Show dialog for screen-specific errors using top-level controller
171-
// Navigation-related errors are handled by FirebaseAuthScreen
172-
if (exception !is AuthException.AccountLinkingRequiredException &&
173-
exception !is AuthException.EmailLinkPromptForEmailException &&
174-
exception !is AuthException.EmailLinkCrossDeviceLinkingException
175-
) {
176-
dialogController?.showErrorDialog(
177-
exception = exception,
178-
onRetry = { ex ->
179-
when (ex) {
180-
is AuthException.InvalidCredentialsException -> {
181-
// User can retry sign in with corrected credentials
182-
}
183-
is AuthException.EmailAlreadyInUseException -> {
184-
// Switch to sign-in mode
185-
mode.value = EmailAuthMode.SignIn
186-
}
187-
else -> Unit
170+
dialogController?.showErrorDialog(
171+
exception = exception,
172+
onRetry = { ex ->
173+
when (ex) {
174+
is AuthException.InvalidCredentialsException -> {
175+
// User can retry sign in with corrected credentials
188176
}
189-
},
190-
onDismiss = {
191-
// Dialog dismissed
177+
178+
is AuthException.EmailAlreadyInUseException -> {
179+
// Switch to sign-in mode
180+
mode.value = EmailAuthMode.SignIn
181+
}
182+
183+
else -> Unit
192184
}
193-
)
194-
}
185+
},
186+
onDismiss = {
187+
// Dialog dismissed
188+
}
189+
)
195190
}
196191

197192
is AuthState.Cancelled -> {
@@ -263,6 +258,23 @@ fun EmailAuthScreen(
263258
}
264259
}
265260
},
261+
onSignInEmailLinkClick = {
262+
coroutineScope.launch {
263+
try {
264+
if (provider.isEmailLinkSignInEnabled) {
265+
authUI.sendSignInLinkToEmail(
266+
context = context,
267+
config = configuration,
268+
provider = provider,
269+
email = emailTextValue.value,
270+
credentialForLinking = authCredentialForLinking,
271+
)
272+
}
273+
} catch (e: Exception) {
274+
onError(AuthException.from(e))
275+
}
276+
}
277+
},
266278
onSignUpClick = {
267279
coroutineScope.launch {
268280
try {
@@ -302,7 +314,7 @@ fun EmailAuthScreen(
302314
onGoToResetPassword = {
303315
textValues.forEach { it.value = "" }
304316
mode.value = EmailAuthMode.ResetPassword
305-
}
317+
},
306318
)
307319

308320
if (content != null) {
@@ -320,7 +332,7 @@ fun EmailAuthScreen(
320332
private fun DefaultEmailAuthContent(
321333
configuration: AuthUIConfiguration,
322334
state: EmailAuthContentState,
323-
onCancel: () -> Unit
335+
onCancel: () -> Unit,
324336
) {
325337
when (state.mode) {
326338
EmailAuthMode.SignIn -> {
@@ -333,6 +345,7 @@ private fun DefaultEmailAuthContent(
333345
onEmailChange = state.onEmailChange,
334346
onPasswordChange = state.onPasswordChange,
335347
onSignInClick = state.onSignInClick,
348+
onSignInWithEmailLink = state.onSignInEmailLinkClick,
336349
onGoToSignUp = state.onGoToSignUp,
337350
onGoToResetPassword = state.onGoToResetPassword,
338351
onNavigateBack = onCancel

auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/email/SignInUI.kt

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Column
1818
import androidx.compose.foundation.layout.PaddingValues
1919
import androidx.compose.foundation.layout.Row
2020
import androidx.compose.foundation.layout.Spacer
21+
import androidx.compose.foundation.layout.fillMaxWidth
2122
import androidx.compose.foundation.layout.height
2223
import androidx.compose.foundation.layout.padding
2324
import androidx.compose.foundation.layout.safeDrawingPadding
@@ -31,6 +32,7 @@ import androidx.compose.material3.AlertDialog
3132
import androidx.compose.material3.Button
3233
import androidx.compose.material3.CircularProgressIndicator
3334
import androidx.compose.material3.ExperimentalMaterial3Api
35+
import androidx.compose.material3.HorizontalDivider
3436
import androidx.compose.material3.Icon
3537
import androidx.compose.material3.IconButton
3638
import androidx.compose.material3.MaterialTheme
@@ -75,22 +77,27 @@ fun SignInUI(
7577
onEmailChange: (String) -> Unit,
7678
onPasswordChange: (String) -> Unit,
7779
onSignInClick: () -> Unit,
80+
onSignInWithEmailLink: () -> Unit,
7881
onGoToSignUp: () -> Unit,
7982
onGoToResetPassword: () -> Unit,
8083
onNavigateBack: (() -> Unit)? = null,
8184
) {
8285
val provider = configuration.providers.filterIsInstance<AuthProvider.Email>().first()
8386
val stringProvider = LocalAuthUIStringProvider.current
87+
88+
// Local state to track if user chose email link sign-in
89+
val isEmailLinkMode = remember { mutableStateOf(false) }
90+
8491
val emailValidator = remember { EmailValidator(stringProvider) }
85-
val passwordValidator = remember {
92+
val passwordValidator = remember(isEmailLinkMode.value) {
8693
PasswordValidator(stringProvider = stringProvider, rules = emptyList())
8794
}
8895

89-
val isFormValid = remember(email, password) {
96+
val isFormValid = remember(email, password, isEmailLinkMode.value) {
9097
derivedStateOf {
9198
listOf(
9299
emailValidator.validate(email),
93-
if (!provider.isEmailLinkSignInEnabled)
100+
if (!isEmailLinkMode.value)
94101
passwordValidator.validate(password) else true,
95102
).all { it }
96103
}
@@ -176,7 +183,7 @@ fun SignInUI(
176183
}
177184
)
178185
Spacer(modifier = Modifier.height(16.dp))
179-
if (!provider.isEmailLinkSignInEnabled) {
186+
if (!isEmailLinkMode.value) {
180187
AuthTextField(
181188
value = password,
182189
validator = passwordValidator,
@@ -191,7 +198,7 @@ fun SignInUI(
191198
)
192199
Spacer(modifier = Modifier.height(8.dp))
193200
}
194-
if (!provider.isEmailLinkSignInEnabled) {
201+
if (!isEmailLinkMode.value) {
195202
TextButton(
196203
modifier = Modifier
197204
.align(Alignment.Start),
@@ -216,7 +223,7 @@ fun SignInUI(
216223
.align(Alignment.End),
217224
) {
218225
// Signup is hidden for email link sign in
219-
if (!provider.isEmailLinkSignInEnabled) {
226+
if (!isEmailLinkMode.value) {
220227
Button(
221228
onClick = {
222229
onGoToSignUp()
@@ -232,7 +239,11 @@ fun SignInUI(
232239
// TODO(demolaf): When signIn is fired if Exception is UserNotFound
233240
// then we check if provider.isNewAccountsAllowed then we show signUp
234241
// else we show an error dialog stating signup is not allowed
235-
onSignInClick()
242+
if (!isEmailLinkMode.value) {
243+
onSignInClick()
244+
} else {
245+
onSignInWithEmailLink()
246+
}
236247
},
237248
enabled = !isLoading && isFormValid.value,
238249
) {
@@ -246,6 +257,44 @@ fun SignInUI(
246257
}
247258
}
248259
}
260+
261+
// Show toggle between password and email link sign-in
262+
if (provider.isEmailLinkSignInEnabled) {
263+
Spacer(modifier = Modifier.height(64.dp))
264+
Row(
265+
modifier = Modifier.fillMaxWidth(),
266+
verticalAlignment = Alignment.CenterVertically
267+
) {
268+
HorizontalDivider(modifier = Modifier.weight(1f))
269+
Text(
270+
text = "or Continue with",
271+
modifier = Modifier.padding(horizontal = 8.dp),
272+
style = MaterialTheme.typography.bodySmall
273+
)
274+
HorizontalDivider(modifier = Modifier.weight(1f))
275+
}
276+
Spacer(modifier = Modifier.height(24.dp))
277+
Button(
278+
onClick = {
279+
isEmailLinkMode.value = !isEmailLinkMode.value
280+
if (isEmailLinkMode.value) {
281+
// Switching to email link mode: clear password
282+
onPasswordChange("")
283+
}
284+
},
285+
modifier = Modifier.fillMaxWidth(),
286+
enabled = !isLoading
287+
) {
288+
Text(
289+
if (isEmailLinkMode.value) {
290+
"Sign in with password".uppercase()
291+
} else {
292+
"Sign in with email link".uppercase()
293+
}
294+
)
295+
}
296+
}
297+
249298
Spacer(modifier = Modifier.height(16.dp))
250299
TermsAndPrivacyForm(
251300
modifier = Modifier.align(Alignment.End),
@@ -285,6 +334,7 @@ fun PreviewSignInUI() {
285334
onEmailChange = { email -> },
286335
onPasswordChange = { password -> },
287336
onSignInClick = {},
337+
onSignInWithEmailLink = {},
288338
onGoToSignUp = {},
289339
onGoToResetPassword = {},
290340
)

composeapp/src/main/java/com/firebase/composeapp/HighLevelApiDemoActivity.kt

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,6 @@ class HighLevelApiDemoActivity : ComponentActivity() {
5656
serverClientId = "771411398215-o39fujhds88bs4mb5ai7u6o73g86fspp.apps.googleusercontent.com",
5757
)
5858
)
59-
// Email/Password Sign-in
60-
provider(
61-
AuthProvider.Email(
62-
isDisplayNameRequired = true,
63-
isEmailLinkSignInEnabled = false,
64-
isNewAccountsAllowed = true,
65-
minimumPasswordLength = 8,
66-
passwordValidationRules = listOf(
67-
PasswordRule.MinimumLength(8),
68-
PasswordRule.RequireLowercase,
69-
PasswordRule.RequireUppercase,
70-
),
71-
emailLinkActionCodeSettings = null,
72-
buttonLabel = "Sign in with Email"
73-
)
74-
)
75-
// Email Link Sign-in (passwordless)
7659
provider(
7760
AuthProvider.Email(
7861
isDisplayNameRequired = true,
@@ -94,7 +77,6 @@ class HighLevelApiDemoActivity : ComponentActivity() {
9477
PasswordRule.RequireLowercase,
9578
PasswordRule.RequireUppercase,
9679
),
97-
buttonLabel = "Sign in with Email Link"
9880
)
9981
)
10082
provider(

e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/EmailAuthScreenTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ class EmailAuthScreenTest {
558558
onEmailChange = state.onEmailChange,
559559
onPasswordChange = state.onPasswordChange,
560560
onSignInClick = state.onSignInClick,
561+
onSignInWithEmailLink = state.onSignInEmailLinkClick,
561562
onGoToSignUp = state.onGoToSignUp,
562563
onGoToResetPassword = state.onGoToResetPassword,
563564
)

0 commit comments

Comments
 (0)