Skip to content

Commit e92790d

Browse files
demolafthatfiredev
andauthored
feat: use CredentialManager to fetch and store credential for Email authentication (#2286)
* feat: implement credential manager for saving and retrieving user credentials * feat: add "Continue as..." feature to enhance user experience with last-used sign-in preferences * feat: add "Continue with..." options for various sign-in methods in strings.xml * fix: add applicationContext parameter to signInWithProvider and submitVerificationCode tests --------- Co-authored-by: Rosário P. Fernandes <[email protected]>
1 parent 458ad1d commit e92790d

File tree

107 files changed

+1837
-41
lines changed

Some content is hidden

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

107 files changed

+1837
-41
lines changed

app/src/main/java/com/firebaseui/android/demo/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import com.google.firebase.FirebaseApp
3636
*/
3737
class MainActivity : ComponentActivity() {
3838
companion object {
39-
private const val USE_AUTH_EMULATOR = false
39+
private const val USE_AUTH_EMULATOR = true
4040
private const val AUTH_EMULATOR_HOST = "10.0.2.2"
4141
private const val AUTH_EMULATOR_PORT = 9099
4242
}

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

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@ package com.firebase.ui.auth.configuration.auth_provider
1616

1717
import android.content.Context
1818
import android.net.Uri
19+
import android.util.Log
1920
import com.firebase.ui.auth.R
2021
import com.firebase.ui.auth.AuthException
2122
import com.firebase.ui.auth.AuthState
2223
import com.firebase.ui.auth.FirebaseAuthUI
2324
import com.firebase.ui.auth.configuration.AuthUIConfiguration
2425
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Companion.canUpgradeAnonymous
2526
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Companion.mergeProfile
27+
import com.firebase.ui.auth.credentialmanager.PasswordCredentialCancelledException
28+
import com.firebase.ui.auth.credentialmanager.PasswordCredentialException
29+
import com.firebase.ui.auth.credentialmanager.PasswordCredentialHandler
2630
import com.firebase.ui.auth.util.EmailLinkPersistenceManager
2731
import com.firebase.ui.auth.util.EmailLinkParser
2832
import com.firebase.ui.auth.util.PersistenceManager
2933
import com.firebase.ui.auth.util.SessionUtils
34+
import com.firebase.ui.auth.util.SignInPreferenceManager
3035
import com.google.firebase.FirebaseApp
3136
import com.google.firebase.auth.ActionCodeSettings
3237
import com.google.firebase.auth.AuthCredential
@@ -38,6 +43,7 @@ import com.google.firebase.auth.FirebaseAuthUserCollisionException
3843
import kotlinx.coroutines.CancellationException
3944
import kotlinx.coroutines.tasks.await
4045

46+
private const val TAG = "EmailAuthProvider"
4147

4248
/**
4349
* Creates an email/password account or links the credential to an anonymous user.
@@ -160,6 +166,37 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
160166
mergeProfile(auth, name, null)
161167
}
162168
}
169+
170+
// Save credentials to Credential Manager if enabled
171+
if (config.isCredentialManagerEnabled) {
172+
try {
173+
val credentialHandler = PasswordCredentialHandler(context)
174+
credentialHandler.savePassword(email, password)
175+
Log.d(TAG, "Password credential saved successfully for: $email")
176+
} catch (e: PasswordCredentialCancelledException) {
177+
// User cancelled - this is fine, don't break the auth flow
178+
Log.d(TAG, "User cancelled credential save for: $email")
179+
} catch (e: PasswordCredentialException) {
180+
// Failed to save - log but don't break the auth flow
181+
Log.w(TAG, "Failed to save password credential for: $email", e)
182+
}
183+
}
184+
185+
// Save sign-in preference for "Continue as..." feature
186+
if (result != null) {
187+
try {
188+
SignInPreferenceManager.saveLastSignIn(
189+
context = context,
190+
providerId = "password",
191+
identifier = email
192+
)
193+
Log.d(TAG, "Sign-in preference saved for: $email")
194+
} catch (e: Exception) {
195+
// Failed to save preference - log but don't break auth flow
196+
Log.w(TAG, "Failed to save sign-in preference for: $email", e)
197+
}
198+
}
199+
163200
updateAuthState(AuthState.Idle)
164201
return result
165202
} catch (e: FirebaseAuthUserCollisionException) {
@@ -281,6 +318,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword(
281318
email: String,
282319
password: String,
283320
credentialForLinking: AuthCredential? = null,
321+
skipCredentialSave: Boolean = false,
284322
): AuthResult? {
285323
try {
286324
updateAuthState(AuthState.Loading("Signing in..."))
@@ -361,7 +399,38 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword(
361399
result
362400
}
363401
}
364-
}.also {
402+
}.also { result ->
403+
// Save credentials to Credential Manager if enabled
404+
// Skip if user signed in with a retrieved credential (already saved)
405+
if (config.isCredentialManagerEnabled && result != null && !skipCredentialSave) {
406+
try {
407+
val credentialHandler = PasswordCredentialHandler(context)
408+
credentialHandler.savePassword(email, password)
409+
Log.d(TAG, "Password credential saved successfully for: $email")
410+
} catch (e: PasswordCredentialCancelledException) {
411+
// User cancelled - this is fine, don't break the auth flow
412+
Log.d(TAG, "User cancelled credential save for: $email")
413+
} catch (e: PasswordCredentialException) {
414+
// Failed to save - log but don't break the auth flow
415+
Log.w(TAG, "Failed to save password credential for: $email", e)
416+
}
417+
}
418+
419+
// Save sign-in preference for "Continue as..." feature
420+
if (result != null) {
421+
try {
422+
SignInPreferenceManager.saveLastSignIn(
423+
context = context,
424+
providerId = "password",
425+
identifier = email
426+
)
427+
Log.d(TAG, "Sign-in preference saved for: $email")
428+
} catch (e: Exception) {
429+
// Failed to save preference - log but don't break auth flow
430+
Log.w(TAG, "Failed to save sign-in preference for: $email", e)
431+
}
432+
}
433+
365434
updateAuthState(AuthState.Idle)
366435
}
367436
} catch (e: FirebaseAuthMultiFactorException) {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.firebase.ui.auth.AuthState
3232
import com.firebase.ui.auth.FirebaseAuthUI
3333
import com.firebase.ui.auth.configuration.AuthUIConfiguration
3434
import com.firebase.ui.auth.util.EmailLinkPersistenceManager
35+
import com.firebase.ui.auth.util.SignInPreferenceManager
3536
import kotlinx.coroutines.CancellationException
3637
import kotlinx.coroutines.launch
3738

@@ -158,6 +159,23 @@ internal suspend fun FirebaseAuthUI.signInWithFacebook(
158159
displayName = profileData?.displayName,
159160
photoUrl = profileData?.photoUrl,
160161
)
162+
163+
// Save sign-in preference for "Continue as..." feature
164+
try {
165+
val user = auth.currentUser
166+
val identifier = user?.email
167+
if (identifier != null) {
168+
SignInPreferenceManager.saveLastSignIn(
169+
context = context,
170+
providerId = provider.providerId,
171+
identifier = identifier
172+
)
173+
android.util.Log.d("FacebookAuthProvider", "Sign-in preference saved for: $identifier")
174+
}
175+
} catch (e: Exception) {
176+
// Failed to save preference - log but don't break auth flow
177+
android.util.Log.w("FacebookAuthProvider", "Failed to save sign-in preference", e)
178+
}
161179
} catch (e: AuthException.AccountLinkingRequiredException) {
162180
// Account collision occurred - save Facebook credential for linking after email link sign-in
163181
// This happens when a user tries to sign in with Facebook but an email link account exists

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.firebase.ui.auth.AuthState
1313
import com.firebase.ui.auth.FirebaseAuthUI
1414
import com.firebase.ui.auth.configuration.AuthUIConfiguration
1515
import com.firebase.ui.auth.util.EmailLinkPersistenceManager
16+
import com.firebase.ui.auth.util.SignInPreferenceManager
1617
import com.google.android.gms.common.api.Scope
1718
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
1819
import kotlinx.coroutines.CancellationException
@@ -149,6 +150,23 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle(
149150
displayName = result.displayName,
150151
photoUrl = result.photoUrl,
151152
)
153+
154+
// Save sign-in preference for "Continue as..." feature
155+
try {
156+
val user = auth.currentUser
157+
val identifier = user?.email
158+
if (identifier != null) {
159+
SignInPreferenceManager.saveLastSignIn(
160+
context = context,
161+
providerId = provider.providerId,
162+
identifier = identifier
163+
)
164+
android.util.Log.d("GoogleAuthProvider", "Sign-in preference saved for: $identifier")
165+
}
166+
} catch (e: Exception) {
167+
// Failed to save preference - log but don't break auth flow
168+
android.util.Log.w("GoogleAuthProvider", "Failed to save sign-in preference", e)
169+
}
152170
} catch (e: AuthException.AccountLinkingRequiredException) {
153171
// Account collision occurred - save Facebook credential for linking after email link sign-in
154172
// This happens when a user tries to sign in with Facebook but an email link account exists

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

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

33
import android.app.Activity
4+
import android.content.Context
45
import androidx.compose.runtime.Composable
56
import androidx.compose.runtime.remember
67
import androidx.compose.runtime.rememberCoroutineScope
@@ -9,6 +10,7 @@ import com.firebase.ui.auth.AuthState
910
import com.firebase.ui.auth.FirebaseAuthUI
1011
import com.firebase.ui.auth.configuration.AuthUIConfiguration
1112
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Companion.canUpgradeAnonymous
13+
import com.firebase.ui.auth.util.SignInPreferenceManager
1214
import com.google.firebase.auth.FirebaseAuthUserCollisionException
1315
import com.google.firebase.auth.OAuthCredential
1416
import com.google.firebase.auth.OAuthProvider
@@ -48,6 +50,7 @@ import kotlinx.coroutines.tasks.await
4850
*/
4951
@Composable
5052
internal fun FirebaseAuthUI.rememberOAuthSignInHandler(
53+
context: Context,
5154
activity: Activity?,
5255
config: AuthUIConfiguration,
5356
provider: AuthProvider.OAuth,
@@ -63,6 +66,7 @@ internal fun FirebaseAuthUI.rememberOAuthSignInHandler(
6366
coroutineScope.launch {
6467
try {
6568
signInWithProvider(
69+
context = context,
6670
config = config,
6771
activity = activity,
6872
provider = provider
@@ -119,6 +123,7 @@ internal fun FirebaseAuthUI.rememberOAuthSignInHandler(
119123
* @see signInAndLinkWithCredential
120124
*/
121125
internal suspend fun FirebaseAuthUI.signInWithProvider(
126+
context: Context,
122127
config: AuthUIConfiguration,
123128
activity: Activity,
124129
provider: AuthProvider.OAuth,
@@ -172,6 +177,24 @@ internal suspend fun FirebaseAuthUI.signInWithProvider(
172177
val credential = authResult?.credential as? OAuthCredential
173178
if (credential != null) {
174179
// The user is already signed in via startActivityForSignInWithProvider/startActivityForLinkWithProvider
180+
181+
// Save sign-in preference for "Continue as..." feature
182+
try {
183+
val user = auth.currentUser
184+
val identifier = user?.email
185+
if (identifier != null) {
186+
SignInPreferenceManager.saveLastSignIn(
187+
context = context,
188+
providerId = provider.providerId,
189+
identifier = identifier
190+
)
191+
android.util.Log.d("OAuthProvider", "Sign-in preference saved for: $identifier (${provider.providerId})")
192+
}
193+
} catch (e: Exception) {
194+
// Failed to save preference - log but don't break auth flow
195+
android.util.Log.w("OAuthProvider", "Failed to save sign-in preference", e)
196+
}
197+
175198
// Just update state to Idle
176199
updateAuthState(AuthState.Idle)
177200
} else {

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.firebase.ui.auth.configuration.auth_provider
22

33
import android.app.Activity
4+
import android.content.Context
45
import com.firebase.ui.auth.AuthException
56
import com.firebase.ui.auth.AuthState
67
import com.firebase.ui.auth.FirebaseAuthUI
78
import com.firebase.ui.auth.configuration.AuthUIConfiguration
9+
import com.firebase.ui.auth.util.SignInPreferenceManager
810
import com.google.firebase.auth.AuthResult
911
import com.google.firebase.auth.MultiFactorSession
1012
import com.google.firebase.auth.PhoneAuthCredential
@@ -197,6 +199,7 @@ internal suspend fun FirebaseAuthUI.verifyPhoneNumber(
197199
* @throws AuthException.NetworkException if a network error occurs
198200
*/
199201
internal suspend fun FirebaseAuthUI.submitVerificationCode(
202+
context: Context,
200203
config: AuthUIConfiguration,
201204
verificationId: String,
202205
code: String,
@@ -206,6 +209,7 @@ internal suspend fun FirebaseAuthUI.submitVerificationCode(
206209
updateAuthState(AuthState.Loading("Submitting verification code..."))
207210
val credential = credentialProvider.getCredential(verificationId, code)
208211
return signInWithPhoneAuthCredential(
212+
context = context,
209213
config = config,
210214
credential = credential
211215
)
@@ -288,15 +292,37 @@ internal suspend fun FirebaseAuthUI.submitVerificationCode(
288292
* @throws AuthException.NetworkException if a network error occurs
289293
*/
290294
internal suspend fun FirebaseAuthUI.signInWithPhoneAuthCredential(
295+
context: Context,
291296
config: AuthUIConfiguration,
292297
credential: PhoneAuthCredential,
293298
): AuthResult? {
294299
try {
295300
updateAuthState(AuthState.Loading("Signing in with phone..."))
296-
return signInAndLinkWithCredential(
301+
val result = signInAndLinkWithCredential(
297302
config = config,
298303
credential = credential,
299304
)
305+
306+
// Save sign-in preference for "Continue as..." feature
307+
if (result != null) {
308+
try {
309+
val user = auth.currentUser
310+
val identifier = user?.phoneNumber
311+
if (identifier != null) {
312+
SignInPreferenceManager.saveLastSignIn(
313+
context = context,
314+
providerId = "phone",
315+
identifier = identifier
316+
)
317+
android.util.Log.d("PhoneAuthProvider", "Sign-in preference saved for: $identifier")
318+
}
319+
} catch (e: Exception) {
320+
// Failed to save preference - log but don't break auth flow
321+
android.util.Log.w("PhoneAuthProvider", "Failed to save sign-in preference", e)
322+
}
323+
}
324+
325+
return result
300326
} catch (e: CancellationException) {
301327
val cancelledException = AuthException.AuthCancelledException(
302328
message = "Sign in with phone was cancelled",

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,33 @@ interface AuthUIStringProvider {
9797
/** Button text for Yahoo sign-in option */
9898
val signInWithYahoo: String
9999

100+
/** Button text for Google continue option */
101+
val continueWithGoogle: String
102+
103+
/** Button text for Facebook continue option */
104+
val continueWithFacebook: String
105+
106+
/** Button text for Twitter continue option */
107+
val continueWithTwitter: String
108+
109+
/** Button text for Github continue option */
110+
val continueWithGithub: String
111+
112+
/** Button text for Email continue option */
113+
val continueWithEmail: String
114+
115+
/** Button text for Phone continue option */
116+
val continueWithPhone: String
117+
118+
/** Button text for Apple continue option */
119+
val continueWithApple: String
120+
121+
/** Button text for Microsoft continue option */
122+
val continueWithMicrosoft: String
123+
124+
/** Button text for Yahoo continue option */
125+
val continueWithYahoo: String
126+
100127
/** Error message when email address field is empty */
101128
val missingEmailAddress: String
102129

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ class DefaultAuthUIStringProvider(
8080
override val signInWithYahoo: String
8181
get() = localizedContext.getString(R.string.fui_sign_in_with_yahoo)
8282

83+
/**
84+
* Auth Provider "Continue With" Button Strings
85+
*/
86+
override val continueWithGoogle: String
87+
get() = localizedContext.getString(R.string.fui_continue_with_google)
88+
override val continueWithFacebook: String
89+
get() = localizedContext.getString(R.string.fui_continue_with_facebook)
90+
override val continueWithTwitter: String
91+
get() = localizedContext.getString(R.string.fui_continue_with_twitter)
92+
override val continueWithGithub: String
93+
get() = localizedContext.getString(R.string.fui_continue_with_github)
94+
override val continueWithEmail: String
95+
get() = localizedContext.getString(R.string.fui_continue_with_email)
96+
override val continueWithPhone: String
97+
get() = localizedContext.getString(R.string.fui_continue_with_phone)
98+
override val continueWithApple: String
99+
get() = localizedContext.getString(R.string.fui_continue_with_apple)
100+
override val continueWithMicrosoft: String
101+
get() = localizedContext.getString(R.string.fui_continue_with_microsoft)
102+
override val continueWithYahoo: String
103+
get() = localizedContext.getString(R.string.fui_continue_with_yahoo)
104+
83105
/**
84106
* Email Validator Strings
85107
*/

0 commit comments

Comments
 (0)