diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1bc54ae34..60ccc6600 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ +> @@ -41,7 +41,7 @@ android:label="@string/title_auth_activity" /> + android:label="@string/title_anonymous_upgrade" /> - + \ No newline at end of file diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index d0c873cc8..66df263a8 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -78,10 +78,14 @@ dependencies { implementation(Config.Libs.Androidx.customTabs) implementation(Config.Libs.Androidx.constraint) implementation("androidx.credentials:credentials:1.3.0") + implementation("androidx.credentials:credentials-play-services-auth:1.3.0") implementation(Config.Libs.Androidx.lifecycleExtensions) implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1") annotationProcessor(Config.Libs.Androidx.lifecycleCompiler) implementation(platform(Config.Libs.Firebase.bom)) diff --git a/auth/src/main/AndroidManifest.xml b/auth/src/main/AndroidManifest.xml index 0cb0838c4..bb1a19204 100644 --- a/auth/src/main/AndroidManifest.xml +++ b/auth/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ +> @@ -127,4 +127,4 @@ - + \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/KickoffActivity.java b/auth/src/main/java/com/firebase/ui/auth/KickoffActivity.java index 805a09820..3fa619bef 100644 --- a/auth/src/main/java/com/firebase/ui/auth/KickoffActivity.java +++ b/auth/src/main/java/com/firebase/ui/auth/KickoffActivity.java @@ -36,7 +36,7 @@ protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mKickstarter = new ViewModelProvider(this).get(SignInKickstarter.class); mKickstarter.init(getFlowParams()); - mKickstarter.getOperation().observe(this, new ResourceObserver(this) { + mKickstarter.getOperation().observe(this, new ResourceObserver<>(this) { @Override protected void onSuccess(@NonNull IdpResponse response) { finish(RESULT_OK, response.toIntent()); diff --git a/auth/src/main/java/com/firebase/ui/auth/data/model/PendingIntentRequiredException.java b/auth/src/main/java/com/firebase/ui/auth/data/model/PendingIntentRequiredException.java index cc938b6ad..fddd85a95 100644 --- a/auth/src/main/java/com/firebase/ui/auth/data/model/PendingIntentRequiredException.java +++ b/auth/src/main/java/com/firebase/ui/auth/data/model/PendingIntentRequiredException.java @@ -1,6 +1,7 @@ package com.firebase.ui.auth.data.model; import android.app.PendingIntent; +import android.content.IntentSender; import com.firebase.ui.auth.ErrorCodes; import com.firebase.ui.auth.FirebaseUiException; @@ -11,19 +12,53 @@ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class PendingIntentRequiredException extends FirebaseUiException { private final PendingIntent mPendingIntent; + private final IntentSender mIntentSender; private final int mRequestCode; + /** + * Constructor for cases when a PendingIntent is available. + * + * @param pendingIntent The PendingIntent required to complete the operation. + * @param requestCode The associated request code. + */ public PendingIntentRequiredException(@NonNull PendingIntent pendingIntent, int requestCode) { super(ErrorCodes.UNKNOWN_ERROR); mPendingIntent = pendingIntent; + mIntentSender = null; mRequestCode = requestCode; } - @NonNull + /** + * Constructor for cases when an IntentSender is available. + * + * @param intentSender The IntentSender required to complete the operation. + * @param requestCode The associated request code. + */ + public PendingIntentRequiredException(@NonNull IntentSender intentSender, int requestCode) { + super(ErrorCodes.UNKNOWN_ERROR); + mIntentSender = intentSender; + mPendingIntent = null; + mRequestCode = requestCode; + } + + /** + * Returns the PendingIntent, if available. + * + * @return The PendingIntent or null if not available. + */ public PendingIntent getPendingIntent() { return mPendingIntent; } + /** + * Returns the IntentSender, if available. + * + * @return The IntentSender or null if not available. + */ + public IntentSender getIntentSender() { + return mIntentSender; + } + public int getRequestCode() { return mRequestCode; } diff --git a/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.java b/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.java deleted file mode 100644 index 535dffb8a..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.firebase.ui.auth.data.remote; - -import android.app.Activity; -import android.app.Application; -import android.content.Intent; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.ErrorCodes; -import com.firebase.ui.auth.IdpResponse; -import com.firebase.ui.auth.data.model.IntentRequiredException; -import com.firebase.ui.auth.data.model.PendingIntentRequiredException; -import com.firebase.ui.auth.data.model.Resource; -import com.firebase.ui.auth.data.model.User; -import com.firebase.ui.auth.data.model.UserCancellationException; -import com.firebase.ui.auth.ui.email.EmailActivity; -import com.firebase.ui.auth.ui.email.EmailLinkCatcherActivity; -import com.firebase.ui.auth.ui.idp.AuthMethodPickerActivity; -import com.firebase.ui.auth.ui.idp.SingleSignInActivity; -import com.firebase.ui.auth.ui.phone.PhoneActivity; -import com.firebase.ui.auth.util.ExtraConstants; -import com.firebase.ui.auth.util.GoogleApiUtils; -import com.firebase.ui.auth.util.data.ProviderUtils; -import com.firebase.ui.auth.viewmodel.RequestCodes; -import com.firebase.ui.auth.viewmodel.SignInViewModelBase; -import com.google.android.gms.common.api.ApiException; -import com.google.android.gms.common.api.CommonStatusCodes; -import com.google.android.gms.common.api.ResolvableApiException; -import com.google.android.gms.tasks.Task; -import com.google.firebase.auth.AuthResult; -import com.google.firebase.auth.EmailAuthProvider; -import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; -import com.google.firebase.auth.FirebaseAuthInvalidUserException; -import com.google.firebase.auth.GoogleAuthProvider; -import com.google.firebase.auth.PhoneAuthProvider; -import com.google.android.gms.auth.api.identity.BeginSignInRequest; -import com.google.android.gms.auth.api.identity.SignInClient; -import com.google.android.gms.auth.api.identity.SignInCredential; -import com.google.android.gms.auth.api.identity.Identity; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import static com.firebase.ui.auth.AuthUI.EMAIL_LINK_PROVIDER; - -public class SignInKickstarter extends SignInViewModelBase { - private static final String TAG = "SignInKickstarter"; - - public SignInKickstarter(Application application) { - super(application); - } - - public void start() { - if (!TextUtils.isEmpty(getArguments().emailLink)) { - setResult(Resource.forFailure(new IntentRequiredException( - EmailLinkCatcherActivity.createIntent(getApplication(), getArguments()), - RequestCodes.EMAIL_FLOW))); - return; - } - - startAuthMethodChoice(); - } - - private void startAuthMethodChoice() { - if (!getArguments().shouldShowProviderChoice()) { - AuthUI.IdpConfig firstIdpConfig = getArguments().getDefaultOrFirstProvider(); - String firstProvider = firstIdpConfig.getProviderId(); - switch (firstProvider) { - case EMAIL_LINK_PROVIDER: - case EmailAuthProvider.PROVIDER_ID: - setResult(Resource.forFailure(new IntentRequiredException( - EmailActivity.createIntent(getApplication(), getArguments()), - RequestCodes.EMAIL_FLOW))); - break; - case PhoneAuthProvider.PROVIDER_ID: - setResult(Resource.forFailure(new IntentRequiredException( - PhoneActivity.createIntent(getApplication(), getArguments(), firstIdpConfig.getParams()), - RequestCodes.PHONE_FLOW))); - break; - default: - redirectSignIn(firstProvider, null); - break; - } - } else { - setResult(Resource.forFailure(new IntentRequiredException( - AuthMethodPickerActivity.createIntent(getApplication(), getArguments()), - RequestCodes.AUTH_PICKER_FLOW))); - } - } - - private void redirectSignIn(String provider, String id) { - switch (provider) { - case EmailAuthProvider.PROVIDER_ID: - setResult(Resource.forFailure(new IntentRequiredException( - EmailActivity.createIntent(getApplication(), getArguments(), id), - RequestCodes.EMAIL_FLOW))); - break; - case PhoneAuthProvider.PROVIDER_ID: - Bundle args = new Bundle(); - args.putString(ExtraConstants.PHONE, id); - setResult(Resource.forFailure(new IntentRequiredException( - PhoneActivity.createIntent(getApplication(), getArguments(), args), - RequestCodes.PHONE_FLOW))); - break; - default: - setResult(Resource.forFailure(new IntentRequiredException( - SingleSignInActivity.createIntent(getApplication(), getArguments(), - new User.Builder(provider, id).build()), - RequestCodes.PROVIDER_FLOW))); - } - } - - private List getCredentialAccountTypes() { - List accounts = new ArrayList<>(); - for (AuthUI.IdpConfig idpConfig : getArguments().providers) { - @AuthUI.SupportedProvider String providerId = idpConfig.getProviderId(); - if (providerId.equals(GoogleAuthProvider.PROVIDER_ID)) { - accounts.add(ProviderUtils.providerIdToAccountType(providerId)); - } - } - return accounts; - } - - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - switch (requestCode) { - case RequestCodes.CRED_HINT: - if (resultCode == Activity.RESULT_OK && data != null) { - try { - SignInClient signInClient = Identity.getSignInClient(getApplication()); - SignInCredential credential = signInClient.getSignInCredentialFromIntent(data); - handleCredential(credential); - } catch (ApiException e) { - // Optionally log the error - startAuthMethodChoice(); - } - } else { - startAuthMethodChoice(); - } - break; - case RequestCodes.EMAIL_FLOW: - case RequestCodes.AUTH_PICKER_FLOW: - case RequestCodes.PHONE_FLOW: - case RequestCodes.PROVIDER_FLOW: - if (resultCode == RequestCodes.EMAIL_LINK_WRONG_DEVICE_FLOW || - resultCode == RequestCodes.EMAIL_LINK_INVALID_LINK_FLOW) { - startAuthMethodChoice(); - return; - } - IdpResponse response = IdpResponse.fromResultIntent(data); - if (response == null) { - setResult(Resource.forFailure(new UserCancellationException())); - } else if (response.isSuccessful()) { - setResult(Resource.forSuccess(response)); - } else if (response.getError().getErrorCode() == - ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT) { - handleMergeFailure(response); - } else { - setResult(Resource.forFailure(response.getError())); - } - } - } - - /** - * Minimal change: Adapted to work with the new SignInCredential. - */ - private void handleCredential(final SignInCredential credential) { - String id = credential.getId(); - String password = credential.getPassword(); - if (TextUtils.isEmpty(password)) { - // Instead of checking accountType, check for a Google ID token. - String googleIdToken = credential.getGoogleIdToken(); - if (!TextUtils.isEmpty(googleIdToken)) { - final IdpResponse response = new IdpResponse.Builder( - new User.Builder(GoogleAuthProvider.PROVIDER_ID, id).build()).build(); - setResult(Resource.forLoading()); - getAuth().signInWithCredential(GoogleAuthProvider.getCredential(googleIdToken, null)) - .addOnSuccessListener(authResult -> handleSuccess(response, authResult)) - .addOnFailureListener(e -> startAuthMethodChoice()); - } else { - startAuthMethodChoice(); - } - } else { - final IdpResponse response = new IdpResponse.Builder( - new User.Builder(EmailAuthProvider.PROVIDER_ID, id).build()).build(); - setResult(Resource.forLoading()); - getAuth().signInWithEmailAndPassword(id, password) - .addOnSuccessListener(authResult -> handleSuccess(response, authResult)) - .addOnFailureListener(e -> { - if (e instanceof FirebaseAuthInvalidUserException || - e instanceof FirebaseAuthInvalidCredentialsException) { - // Minimal change: sign out using the new API (delete isn’t available). - Identity.getSignInClient(getApplication()).signOut(); - } - startAuthMethodChoice(); - }); - } - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.kt b/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.kt new file mode 100644 index 000000000..b665ce1d4 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.kt @@ -0,0 +1,247 @@ +package com.firebase.ui.auth.data.remote + +import android.app.Activity +import android.app.Application +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.ErrorCodes +import com.firebase.ui.auth.IdpResponse +import com.firebase.ui.auth.data.model.IntentRequiredException +import com.firebase.ui.auth.data.model.Resource +import com.firebase.ui.auth.data.model.User +import com.firebase.ui.auth.data.model.UserCancellationException +import com.firebase.ui.auth.ui.email.EmailActivity +import com.firebase.ui.auth.ui.email.EmailLinkCatcherActivity +import com.firebase.ui.auth.ui.idp.AuthMethodPickerActivity +import com.firebase.ui.auth.ui.idp.SingleSignInActivity +import com.firebase.ui.auth.ui.phone.PhoneActivity +import com.firebase.ui.auth.util.ExtraConstants +import com.firebase.ui.auth.viewmodel.RequestCodes +import com.firebase.ui.auth.viewmodel.SignInViewModelBase +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.auth.api.identity.SignInCredential +import com.google.android.gms.common.api.ApiException +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.EmailAuthProvider +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException +import com.google.firebase.auth.FirebaseAuthInvalidUserException +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.PhoneAuthProvider +import kotlinx.coroutines.launch +import androidx.lifecycle.viewModelScope + +import androidx.credentials.Credential +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetPasswordOption +import androidx.credentials.PasswordCredential +import androidx.credentials.PublicKeyCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException + + +private const val TAG = "SignInKickstarter" + +class SignInKickstarter(application: Application?) : SignInViewModelBase(application) { + + private val app: Application = checkNotNull(application) + + /** + * Entry point. If an email link is detected, immediately launch the email catcher. + * Otherwise, launch startAuthMethodChoice. + */ + fun start() { + if (!TextUtils.isEmpty(arguments.emailLink)) { + setResult( + Resource.forFailure( + IntentRequiredException( + EmailLinkCatcherActivity.createIntent(app, arguments), + RequestCodes.EMAIL_FLOW + ) + ) + ) + return + } + startAuthMethodChoice() + } + + + /** + * Fallback: if no credential was obtained (or after a failed Credential Manager attempt) + * choose the proper sign‑in flow. + */ + private fun startAuthMethodChoice() { + if (!arguments.shouldShowProviderChoice()) { + val firstIdpConfig = arguments.defaultOrFirstProvider + val firstProvider = firstIdpConfig.providerId + when (firstProvider) { + AuthUI.EMAIL_LINK_PROVIDER, EmailAuthProvider.PROVIDER_ID -> + setResult( + Resource.forFailure( + IntentRequiredException( + EmailActivity.createIntent(app, arguments), + RequestCodes.EMAIL_FLOW + ) + ) + ) + PhoneAuthProvider.PROVIDER_ID -> + setResult( + Resource.forFailure( + IntentRequiredException( + PhoneActivity.createIntent(app, arguments, firstIdpConfig.params), + RequestCodes.PHONE_FLOW + ) + ) + ) + else -> redirectSignIn(firstProvider, null) + } + } else { + setResult( + Resource.forFailure( + IntentRequiredException( + AuthMethodPickerActivity.createIntent(app, arguments), + RequestCodes.AUTH_PICKER_FLOW + ) + ) + ) + } + } + + /** + * Helper to route to the proper sign‑in activity for a given provider. + */ + private fun redirectSignIn(provider: String, id: String?) { + when (provider) { + EmailAuthProvider.PROVIDER_ID -> + setResult( + Resource.forFailure( + IntentRequiredException( + EmailActivity.createIntent(app, arguments, id), + RequestCodes.EMAIL_FLOW + ) + ) + ) + PhoneAuthProvider.PROVIDER_ID -> { + val args = Bundle().apply { putString(ExtraConstants.PHONE, id) } + setResult( + Resource.forFailure( + IntentRequiredException( + PhoneActivity.createIntent(app, arguments, args), + RequestCodes.PHONE_FLOW + ) + ) + ) + } + else -> + setResult( + Resource.forFailure( + IntentRequiredException( + SingleSignInActivity.createIntent( + app, arguments, User.Builder(provider, id).build() + ), + RequestCodes.PROVIDER_FLOW + ) + ) + ) + } + } + + /** + * Legacy onActivityResult handler for other flows. + */ + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + RequestCodes.EMAIL_FLOW, + RequestCodes.AUTH_PICKER_FLOW, + RequestCodes.PHONE_FLOW, + RequestCodes.PROVIDER_FLOW -> { + if (resultCode == RequestCodes.EMAIL_LINK_WRONG_DEVICE_FLOW || + resultCode == RequestCodes.EMAIL_LINK_INVALID_LINK_FLOW + ) { + startAuthMethodChoice() + return + } + val response = IdpResponse.fromResultIntent(data) + if (response == null) { + setResult(Resource.forFailure(UserCancellationException())) + } else if (response.isSuccessful) { + setResult(Resource.forSuccess(response)) + } else if (response.error!!.errorCode == ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT) { + handleMergeFailure(response) + } else { + setResult(Resource.forFailure(response.error!!)) + } + } + else -> startAuthMethodChoice() + } + } + + /** + * Handle a successfully returned Credential from the Credential Manager. + */ + private fun handleCredentialManagerResult(credential: Credential) { + when (credential) { + is PasswordCredential -> { + val username = credential.id + val password = credential.password + val response = IdpResponse.Builder( + User.Builder(EmailAuthProvider.PROVIDER_ID, username).build() + ).build() + setResult(Resource.forLoading()) + auth.signInWithEmailAndPassword(username, password) + .addOnSuccessListener { authResult: AuthResult -> + handleSuccess(response, authResult) + // (Optionally finish the hosting activity here.) + } + .addOnFailureListener { e -> + if (e is FirebaseAuthInvalidUserException || + e is FirebaseAuthInvalidCredentialsException + ) { + // Sign out using the new API. + Identity.getSignInClient(app).signOut() + } + startAuthMethodChoice() + } + } + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + auth.signInWithCredential( + GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null) + ) + .addOnSuccessListener { authResult: AuthResult -> + val response = IdpResponse.Builder( + User.Builder( + GoogleAuthProvider.PROVIDER_ID, + // Assume the credential data contains the email. + googleIdTokenCredential.data.getString("email") + ).build() + ) + .setToken(googleIdTokenCredential.idToken) + .build() + handleSuccess(response, authResult) + } + .addOnFailureListener { e -> + Log.e(TAG, "Failed to sign in with Google ID token", e) + startAuthMethodChoice() + } + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) + startAuthMethodChoice() + } + } else { + Log.e(TAG, "Unexpected type of credential") + startAuthMethodChoice() + } + } + else -> { + Log.e(TAG, "Unexpected type of credential") + startAuthMethodChoice() + } + } + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/HelperActivityBase.java b/auth/src/main/java/com/firebase/ui/auth/ui/HelperActivityBase.java index cd82c54d0..96714101b 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/HelperActivityBase.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/HelperActivityBase.java @@ -10,12 +10,8 @@ import com.firebase.ui.auth.IdpResponse; import com.firebase.ui.auth.data.model.FlowParameters; import com.firebase.ui.auth.ui.credentials.CredentialSaveActivity; -import com.firebase.ui.auth.ui.idp.AuthMethodPickerActivity; -import com.firebase.ui.auth.util.CredentialUtils; import com.firebase.ui.auth.util.ExtraConstants; -import com.firebase.ui.auth.util.data.ProviderUtils; import com.firebase.ui.auth.viewmodel.RequestCodes; -import com.google.android.gms.auth.api.credentials.Credential; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; @@ -46,7 +42,7 @@ protected static Intent createBaseIntent( @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - // Forward the results of Smart Lock saving + // Forward the results of CredentialManager saving if (requestCode == RequestCodes.CRED_SAVE_FLOW || resultCode == ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT) { finish(resultCode, data); @@ -73,18 +69,29 @@ public void finish(int resultCode, @Nullable Intent intent) { finish(); } + /** + * Starts the CredentialManager save flow. + * + *

Instead of building a SmartLock {@link com.google.android.gms.auth.api.credentials.Credential}, + * we now extract the user's email (or phone number as a fallback) and pass it along with the + * password and response.

+ * + * @param firebaseUser the currently signed-in user. + * @param response the IdP response. + * @param password the password used during sign-in (may be {@code null}). + */ public void startSaveCredentials( FirebaseUser firebaseUser, IdpResponse response, @Nullable String password) { - // Build credential - String accountType = ProviderUtils.idpResponseToAccountType(response); - Credential credential = CredentialUtils.buildCredential( - firebaseUser, password, accountType); - - // Start the dedicated SmartLock Activity + // Extract email; if null, fallback to the phone number. + String email = firebaseUser.getEmail(); + if (email == null) { + email = firebaseUser.getPhoneNumber(); + } + // Start the dedicated CredentialManager Activity. Intent intent = CredentialSaveActivity.createIntent( - this, getFlowParams(), credential, response); + this, getFlowParams(), email, password, response); startActivityForResult(intent, RequestCodes.CRED_SAVE_FLOW); } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt b/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt index 6c2aa6506..c539de0c7 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log +import androidx.lifecycle.ViewModelProvider import com.firebase.ui.auth.IdpResponse import com.firebase.ui.auth.data.model.FlowParameters import com.firebase.ui.auth.data.model.Resource @@ -11,42 +12,37 @@ import com.firebase.ui.auth.ui.InvisibleActivityBase import com.firebase.ui.auth.util.ExtraConstants import com.firebase.ui.auth.viewmodel.ResourceObserver import com.firebase.ui.auth.viewmodel.credentialmanager.CredentialManagerHandler -import com.google.android.gms.auth.api.credentials.Credential -import androidx.lifecycle.ViewModelProvider import com.google.firebase.auth.FirebaseAuth class CredentialSaveActivity : InvisibleActivityBase() { - - private lateinit var credentialManagerHandler: CredentialManagerHandler override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val response: IdpResponse? = intent.getParcelableExtra(ExtraConstants.IDP_RESPONSE) - val credential: Credential? = intent.getParcelableExtra(ExtraConstants.CREDENTIAL) + val emailExtra: String? = intent.getStringExtra(ExtraConstants.EMAIL) + val password: String? = intent.getStringExtra(ExtraConstants.PASSWORD) credentialManagerHandler = ViewModelProvider(this) .get(CredentialManagerHandler::class.java) .apply { - // Initialize with flow parameters + // Initialize with flow parameters. init(flowParams) - // If we have an IdpResponse, set it so subsequent operations can report results + // Pass the IdP response if present. response?.let { setResponse(it) } - // Observe the operation resource + // Observe the operation's result. operation.observe( this@CredentialSaveActivity, object : ResourceObserver(this@CredentialSaveActivity) { override fun onSuccess(response: IdpResponse) { - // Done saving – success finish(RESULT_OK, response.toIntent()) } override fun onFailure(e: Exception) { - // We don’t want to block the sign-in flow just because saving failed, - // so return RESULT_OK + // Even if saving fails, do not block the sign-in flow. response?.let { finish(RESULT_OK, it.toIntent()) } ?: finish(RESULT_OK, null) @@ -59,14 +55,11 @@ class CredentialSaveActivity : InvisibleActivityBase() { if (currentOp == null) { Log.d(TAG, "Launching save operation.") - // In the old SmartLock flow, you saved a `Credential`; - // with CredentialManager, we typically need email & password for the new request. - // Example usage: pass the current user & the password. - // Adjust as needed for passkeys or other flows. + // With the new CredentialManager, pass the email and password directly. val firebaseUser = FirebaseAuth.getInstance().currentUser - val password = credential?.password + val email = firebaseUser?.email ?: emailExtra - credentialManagerHandler.saveCredentials(this, firebaseUser, password) + credentialManagerHandler.saveCredentials(this, firebaseUser, email, password) } else { Log.d(TAG, "Save operation in progress, doing nothing.") } @@ -79,11 +72,13 @@ class CredentialSaveActivity : InvisibleActivityBase() { fun createIntent( context: Context, flowParams: FlowParameters, - credential: Credential, + email: String, + password: String?, response: IdpResponse ): Intent { return createBaseIntent(context, CredentialSaveActivity::class.java, flowParams).apply { - putExtra(ExtraConstants.CREDENTIAL, credential) + putExtra(ExtraConstants.EMAIL, email) + putExtra(ExtraConstants.PASSWORD, password) putExtra(ExtraConstants.IDP_RESPONSE, response) } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java index 4c8b1c64d..7b572a9df 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java @@ -12,8 +12,7 @@ import android.widget.ProgressBar; import android.widget.TextView; -import com.firebase.ui.auth.ErrorCodes; -import com.firebase.ui.auth.FirebaseUiException; +import com.firebase.ui.auth.AuthUI; import com.firebase.ui.auth.R; import com.firebase.ui.auth.data.model.FlowParameters; import com.firebase.ui.auth.data.model.User; @@ -22,10 +21,8 @@ import com.firebase.ui.auth.util.data.PrivacyDisclosureUtils; import com.firebase.ui.auth.util.ui.ImeHelper; import com.firebase.ui.auth.util.ui.fieldvalidators.EmailFieldValidator; -import com.firebase.ui.auth.viewmodel.ResourceObserver; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; -import com.google.firebase.FirebaseNetworkException; import com.google.firebase.auth.EmailAuthProvider; import androidx.annotation.NonNull; @@ -48,7 +45,8 @@ public class CheckEmailFragment extends FragmentBase implements public static final String TAG = "CheckEmailFragment"; private CheckEmailHandler mHandler; - private Button mNextButton; + private Button mSignInButton; + private Button mSignUpButton; private ProgressBar mProgressBar; private EditText mEmailEditText; private TextInputLayout mEmailLayout; @@ -72,17 +70,16 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - mNextButton = view.findViewById(R.id.button_next); + mSignInButton = view.findViewById(R.id.button_sign_in); + mSignUpButton = view.findViewById(R.id.button_sign_up); mProgressBar = view.findViewById(R.id.top_progress_bar); - // Email field and validator mEmailLayout = view.findViewById(R.id.email_layout); mEmailEditText = view.findViewById(R.id.email); mEmailFieldValidator = new EmailFieldValidator(mEmailLayout); mEmailLayout.setOnClickListener(this); mEmailEditText.setOnClickListener(this); - // Hide header TextView headerText = view.findViewById(R.id.header_text); if (headerText != null) { headerText.setVisibility(View.GONE); @@ -94,7 +91,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat mEmailEditText.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); } - mNextButton.setOnClickListener(this); + // Set listeners for our new sign‑in and sign‑up buttons. + mSignInButton.setOnClickListener(this); + mSignUpButton.setOnClickListener(this); TextView termsText = view.findViewById(R.id.email_tos_and_pp_text); TextView footerText = view.findViewById(R.id.email_footer_tos_and_pp_text); @@ -124,54 +123,16 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { } mListener = (CheckEmailListener) activity; - mHandler.getOperation().observe(getViewLifecycleOwner(), new ResourceObserver( - this, R.string.fui_progress_dialog_checking_accounts) { - @Override - protected void onSuccess(@NonNull User user) { - String email = user.getEmail(); - String provider = user.getProviderId(); + // Removed the observer on mHandler.getOperation() since we no longer rely on provider info. + if (savedInstanceState == null) { + String email = getArguments().getString(ExtraConstants.EMAIL); + if (!TextUtils.isEmpty(email)) { mEmailEditText.setText(email); - //noinspection ConstantConditions new user - if (provider == null) { - mListener.onNewUser(new User.Builder(EmailAuthProvider.PROVIDER_ID, email) - .setName(user.getName()) - .setPhotoUri(user.getPhotoUri()) - .build()); - } else if (provider.equals(EmailAuthProvider.PROVIDER_ID) - || provider.equals(EMAIL_LINK_PROVIDER)) { - mListener.onExistingEmailUser(user); - } else { - mListener.onExistingIdpUser(user); - } + // Previously auto-triggering the check is now removed. + } else if (getFlowParams().enableHints) { + mHandler.fetchCredential(); } - - @Override - protected void onFailure(@NonNull Exception e) { - if (e instanceof FirebaseUiException - && ((FirebaseUiException) e).getErrorCode() == ErrorCodes.DEVELOPER_ERROR) { - mListener.onDeveloperFailure(e); - } - - if (e instanceof FirebaseNetworkException) { - Snackbar.make(getView(), getString(R.string.fui_no_internet), Snackbar.LENGTH_SHORT).show(); - } - - // Otherwise just let the user enter their data - } - }); - - if (savedInstanceState != null) { - return; - } - - // Check for email - String email = getArguments().getString(ExtraConstants.EMAIL); - if (!TextUtils.isEmpty(email)) { - mEmailEditText.setText(email); - validateAndProceed(); - } else if (getFlowParams().enableHints) { - mHandler.fetchCredential(); } } @@ -184,8 +145,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onClick(View view) { int id = view.getId(); - if (id == R.id.button_next) { - validateAndProceed(); + if (id == R.id.button_sign_in) { + signIn(); + } else if (id == R.id.button_sign_up) { + signUp(); } else if (id == R.id.email_layout || id == R.id.email) { mEmailLayout.setError(null); } @@ -193,25 +156,52 @@ public void onClick(View view) { @Override public void onDonePressed() { - validateAndProceed(); + // When the user hits “done” on the keyboard, default to sign‑in. + signIn(); + } + + private String getEmailProvider() { + // Iterate through all IdpConfig entries + for (AuthUI.IdpConfig config : getFlowParams().providers) { + // Assuming there is a getter for the provider ID + if (EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD.equals(config.getProviderId())) { + return EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD; + } + } + // Default to standard email/password + return EmailAuthProvider.PROVIDER_ID; + } + + private void signIn() { + String email = mEmailEditText.getText().toString(); + if (mEmailFieldValidator.validate(email)) { + String provider = getEmailProvider(); + User user = new User.Builder(provider, email).build(); + mListener.onExistingEmailUser(user); + } } - private void validateAndProceed() { + private void signUp() { String email = mEmailEditText.getText().toString(); if (mEmailFieldValidator.validate(email)) { - mHandler.fetchProvider(email); + String provider = getEmailProvider(); + User user = new User.Builder(provider, email).build(); + mListener.onNewUser(user); } } @Override public void showProgress(int message) { - mNextButton.setEnabled(false); + // Disable both buttons while progress is showing. + mSignInButton.setEnabled(false); + mSignUpButton.setEnabled(false); mProgressBar.setVisibility(View.VISIBLE); } @Override public void hideProgress() { - mNextButton.setEnabled(true); + mSignInButton.setEnabled(true); + mSignUpButton.setEnabled(true); mProgressBar.setVisibility(View.INVISIBLE); } @@ -221,7 +211,7 @@ public void hideProgress() { interface CheckEmailListener { /** - * Email entered belongs to an existing email user. + * Email entered belongs to an existing email user (sign‑in flow). */ void onExistingEmailUser(User user); @@ -231,7 +221,7 @@ interface CheckEmailListener { void onExistingIdpUser(User user); /** - * Email entered does not belong to an existing user. + * Email entered does not belong to an existing user (sign‑up flow). */ void onNewUser(User user); diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkPromptEmailFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkPromptEmailFragment.java index b14d40731..4e55594db 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkPromptEmailFragment.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkPromptEmailFragment.java @@ -32,6 +32,7 @@ public class EmailLinkPromptEmailFragment extends FragmentBase implements public static final String TAG = "EmailLinkPromptEmailFragment"; private Button mNextButton; + private Button mSignUpButton; private ProgressBar mProgressBar; private EditText mEmailEditText; @@ -55,7 +56,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - mNextButton = view.findViewById(R.id.button_next); + mNextButton = view.findViewById(R.id.button_sign_in); + mSignUpButton = view.findViewById(R.id.button_sign_up); mProgressBar = view.findViewById(R.id.top_progress_bar); mNextButton.setOnClickListener(this); @@ -117,7 +119,7 @@ private void validateEmailAndFinishSignIn() { @Override public void onClick(View view) { int id = view.getId(); - if (id == R.id.button_next) { + if (id == R.id.button_sign_in) { validateEmailAndFinishSignIn(); } else if (id == R.id.email_layout || id == R.id.email) { mEmailLayout.setError(null); diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java deleted file mode 100644 index df5223817..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java +++ /dev/null @@ -1,552 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.firebase.ui.auth.ui.idp; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import com.firebase.ui.auth.AuthMethodPickerLayout; -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.AuthUI.IdpConfig; -import com.firebase.ui.auth.ErrorCodes; -import com.firebase.ui.auth.FirebaseAuthAnonymousUpgradeException; -import com.firebase.ui.auth.FirebaseUiException; -import com.firebase.ui.auth.IdpResponse; -import com.firebase.ui.auth.KickoffActivity; -import com.firebase.ui.auth.R; -import com.firebase.ui.auth.data.model.FlowParameters; -import com.firebase.ui.auth.data.model.Resource; -import com.firebase.ui.auth.data.model.User; -import com.firebase.ui.auth.data.model.UserCancellationException; -import com.firebase.ui.auth.data.remote.AnonymousSignInHandler; -import com.firebase.ui.auth.data.remote.EmailSignInHandler; -import com.firebase.ui.auth.data.remote.FacebookSignInHandler; -import com.firebase.ui.auth.data.remote.GenericIdpSignInHandler; -import com.firebase.ui.auth.data.remote.GoogleSignInHandler; -import com.firebase.ui.auth.data.remote.PhoneSignInHandler; -import com.firebase.ui.auth.ui.AppCompatBase; -import com.firebase.ui.auth.util.ExtraConstants; -import com.firebase.ui.auth.util.data.PrivacyDisclosureUtils; -import com.firebase.ui.auth.util.data.ProviderUtils; -import com.firebase.ui.auth.viewmodel.ProviderSignInBase; -import com.firebase.ui.auth.viewmodel.ResourceObserver; -import com.firebase.ui.auth.viewmodel.idp.SocialProviderResponseHandler; -import com.google.android.gms.auth.api.identity.BeginSignInRequest; -import com.google.android.gms.auth.api.identity.Identity; -import com.google.android.gms.auth.api.identity.SignInClient; -import com.google.android.gms.auth.api.identity.SignInCredential; -import com.google.android.gms.common.api.ApiException; -import com.google.android.material.snackbar.Snackbar; -import com.google.firebase.auth.EmailAuthProvider; -import com.google.firebase.auth.FacebookAuthProvider; -import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; -import com.google.firebase.auth.FirebaseAuthInvalidUserException; -import com.google.firebase.auth.GoogleAuthProvider; -import com.google.firebase.auth.PhoneAuthProvider; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.IntentSenderRequest; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.IdRes; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.ConstraintSet; -import androidx.lifecycle.ViewModelProvider; - -import static com.firebase.ui.auth.util.ExtraConstants.GENERIC_OAUTH_BUTTON_ID; -import static com.firebase.ui.auth.util.ExtraConstants.GENERIC_OAUTH_PROVIDER_ID; -import static com.firebase.ui.auth.AuthUI.EMAIL_LINK_PROVIDER; - -/** - * Presents the list of authentication options for this app to the user. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class AuthMethodPickerActivity extends AppCompatBase { - - private SocialProviderResponseHandler mHandler; - private List> mProviders; - - private ProgressBar mProgressBar; - private ViewGroup mProviderHolder; - - private AuthMethodPickerLayout customLayout; - - public static Intent createIntent(Context context, FlowParameters flowParams) { - return createBaseIntent(context, AuthMethodPickerActivity.class, flowParams); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - FlowParameters params = getFlowParams(); - customLayout = params.authMethodPickerLayout; - - mHandler = new ViewModelProvider(this).get(SocialProviderResponseHandler.class); - mHandler.init(params); - - - mProviders = new ArrayList<>(); - if (customLayout != null) { - setContentView(customLayout.getMainLayout()); - - //Setup using custom layout - populateIdpListCustomLayout(params.providers); - } else { - setContentView(R.layout.fui_auth_method_picker_layout); - - //UI only with default layout - mProgressBar = findViewById(R.id.top_progress_bar); - mProviderHolder = findViewById(R.id.btn_holder); - - populateIdpList(params.providers); - - int logoId = params.logoId; - if (logoId == AuthUI.NO_LOGO) { - findViewById(R.id.logo).setVisibility(View.GONE); - - ConstraintLayout layout = findViewById(R.id.root); - ConstraintSet constraints = new ConstraintSet(); - constraints.clone(layout); - constraints.setHorizontalBias(R.id.container, 0.5f); - constraints.setVerticalBias(R.id.container, 0.5f); - constraints.applyTo(layout); - } else { - ImageView logo = findViewById(R.id.logo); - logo.setImageResource(logoId); - } - } - - boolean tosAndPpConfigured = getFlowParams().isPrivacyPolicyUrlProvided() - && getFlowParams().isTermsOfServiceUrlProvided(); - - int termsTextId = customLayout == null - ? R.id.main_tos_and_pp - : customLayout.getTosPpView(); - - if (termsTextId >= 0) { - TextView termsText = findViewById(termsTextId); - - // No ToS or PP provided, so we should hide the view entirely - if (!tosAndPpConfigured) { - termsText.setVisibility(View.GONE); - } else { - PrivacyDisclosureUtils.setupTermsOfServiceAndPrivacyPolicyText(this, - getFlowParams(), - termsText); - } - } - - //Handler for both - mHandler.getOperation().observe(this, new ResourceObserver( - this, R.string.fui_progress_dialog_signing_in) { - @Override - protected void onSuccess(@NonNull IdpResponse response) { - startSaveCredentials(mHandler.getCurrentUser(), response, null); - } - - @Override - protected void onFailure(@NonNull Exception e) { - if (e instanceof UserCancellationException) { - // User pressed back, there is no error. - return; - } - - if (e instanceof FirebaseAuthAnonymousUpgradeException) { - finish(ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT, - ((FirebaseAuthAnonymousUpgradeException) e).getResponse().toIntent()); - } else if (e instanceof FirebaseUiException) { - FirebaseUiException fue = (FirebaseUiException) e; - finish(RESULT_CANCELED, IdpResponse.from(fue).toIntent()); - } else { - String text = getString(R.string.fui_error_unknown); - Toast.makeText(AuthMethodPickerActivity.this, - text, - Toast.LENGTH_SHORT).show(); - } - } - }); - - attemptCredentialSignIn(); - } - - /** - * This method attempts to automatically sign in via the credentials API. - * It is called after the layout is inflated so that any progress indicators or UI - * (such as the popup background) are already visible. - */ - private void attemptCredentialSignIn() { - FlowParameters args = getFlowParams(); - boolean supportPasswords = - ProviderUtils.getConfigFromIdps(args.providers, EmailAuthProvider.PROVIDER_ID) != null; - List accountTypes = getCredentialAccountTypes(); - boolean willRequestCredentials = supportPasswords || !accountTypes.isEmpty(); - - if (args.enableCredentials && willRequestCredentials) { - SignInClient signInClient = Identity.getSignInClient(getApplication()); - BeginSignInRequest.Builder requestBuilder = BeginSignInRequest.builder(); - if (supportPasswords) { - requestBuilder.setPasswordRequestOptions( - BeginSignInRequest.PasswordRequestOptions.builder() - .setSupported(true) - .build()); - } - if (!accountTypes.isEmpty()) { - requestBuilder.setGoogleIdTokenRequestOptions( - BeginSignInRequest.GoogleIdTokenRequestOptions.builder() - .setSupported(true) - .setServerClientId(getString(R.string.default_web_client_id)) - .build()); - } - BeginSignInRequest signInRequest = requestBuilder.build(); - signInClient.beginSignIn(signInRequest) - .addOnSuccessListener(result -> { - PendingIntent pendingIntent = result.getPendingIntent(); - try { - // Wrap the PendingIntent in an IntentSenderRequest for the new API. - IntentSenderRequest intentSenderRequest = - new IntentSenderRequest.Builder(pendingIntent.getIntentSender()).build(); - // Launch the intent via the ActivityResultLauncher. - credHintLauncher.launch(intentSenderRequest); - } catch (Exception e) { - e.printStackTrace(); - // If launching the pending intent fails, fall back to your manual sign-in UI. - showAuthMethodPicker(); - } - }) - .addOnFailureListener(e -> { - // Hide any progress indicator and fall back to showing the provider list. - hideProgress(); - showAuthMethodPicker(); - }); - } else { - // Credentials not enabled or not applicable – just show the provider list. - showAuthMethodPicker(); - } - } - - // Register for the new Activity Result API to launch an IntentSender. - private final ActivityResultLauncher credHintLauncher = registerForActivityResult( - new ActivityResultContracts.StartIntentSenderForResult(), - (ActivityResult result) -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null) { - try { - SignInClient signInClient = Identity.getSignInClient(this); - // Extract the SignInCredential from the returned intent. - SignInCredential credential = signInClient.getSignInCredentialFromIntent(result.getData()); - handleCredential(credential); - } catch (ApiException e) { - // Optionally log the error and fall back. - e.printStackTrace(); - } - } - } - ); - - private void handleCredential(final SignInCredential credential) { - String id = credential.getId(); - String password = credential.getPassword(); - if (TextUtils.isEmpty(password)) { - // Instead of checking accountType, check for a Google ID token. - String googleIdToken = credential.getGoogleIdToken(); - if (!TextUtils.isEmpty(googleIdToken)) { - final IdpResponse response = new IdpResponse.Builder( - new User.Builder(GoogleAuthProvider.PROVIDER_ID, id).build()).build(); - KickoffActivity.mKickstarter.setResult(Resource.forLoading()); - getAuth().signInWithCredential(GoogleAuthProvider.getCredential(googleIdToken, null)) - .addOnSuccessListener(authResult -> KickoffActivity.mKickstarter.handleSuccess(response, authResult)); - } - } else { - final IdpResponse response = new IdpResponse.Builder( - new User.Builder(EmailAuthProvider.PROVIDER_ID, id).build()).build(); - KickoffActivity.mKickstarter.setResult(Resource.forLoading()); - getAuth().signInWithEmailAndPassword(id, password) - .addOnSuccessListener(authResult -> { - KickoffActivity.mKickstarter.handleSuccess(response, authResult); - finish(); - }) - .addOnFailureListener(e -> { - if (e instanceof FirebaseAuthInvalidUserException || - e instanceof FirebaseAuthInvalidCredentialsException) { - // Minimal change: sign out using the new API (delete isn’t available). - Identity.getSignInClient(getApplication()).signOut(); - } - }); - } - } - - - /** - * Returns the account types to be passed to the credential manager. - * (You can use your existing logic for this.) - */ - private List getCredentialAccountTypes() { - List accounts = new ArrayList<>(); - for (IdpConfig idpConfig : getFlowParams().providers) { - String providerId = idpConfig.getProviderId(); - if (providerId.equals(GoogleAuthProvider.PROVIDER_ID)) { - accounts.add(ProviderUtils.providerIdToAccountType(providerId)); - } - } - return accounts; - } - - /** - * Fallback: show the auth method picker UI. - * This is called if the credentials attempt fails or isn’t applicable. - */ - private void showAuthMethodPicker() { - hideProgress(); - } - - - private void populateIdpList(List providerConfigs) { - - ViewModelProvider supplier = new ViewModelProvider(this); - mProviders = new ArrayList<>(); - for (IdpConfig idpConfig : providerConfigs) { - @LayoutRes int buttonLayout; - - final String providerId = idpConfig.getProviderId(); - switch (providerId) { - case GoogleAuthProvider.PROVIDER_ID: - buttonLayout = R.layout.fui_idp_button_google; - break; - case FacebookAuthProvider.PROVIDER_ID: - buttonLayout = R.layout.fui_idp_button_facebook; - break; - case EMAIL_LINK_PROVIDER: - case EmailAuthProvider.PROVIDER_ID: - buttonLayout = R.layout.fui_provider_button_email; - break; - case PhoneAuthProvider.PROVIDER_ID: - buttonLayout = R.layout.fui_provider_button_phone; - break; - case AuthUI.ANONYMOUS_PROVIDER: - buttonLayout = R.layout.fui_provider_button_anonymous; - break; - default: - if (!TextUtils.isEmpty( - idpConfig.getParams().getString(GENERIC_OAUTH_PROVIDER_ID))) { - buttonLayout = idpConfig.getParams().getInt(GENERIC_OAUTH_BUTTON_ID); - break; - } - throw new IllegalStateException("Unknown provider: " + providerId); - } - - View loginButton = getLayoutInflater().inflate(buttonLayout, mProviderHolder, false); - handleSignInOperation(idpConfig, loginButton); - mProviderHolder.addView(loginButton); - } - } - - private void populateIdpListCustomLayout(List providerConfigs) { - Map providerButtonIds = customLayout.getProvidersButton(); - for (IdpConfig idpConfig : providerConfigs) { - final String providerId = providerOrEmailLinkProvider(idpConfig.getProviderId()); - - Integer buttonResId = providerButtonIds.get(providerId); - if (buttonResId == null) { - throw new IllegalStateException("No button found for auth provider: " + idpConfig.getProviderId()); - } - - @IdRes int buttonId = buttonResId; - View loginButton = findViewById(buttonId); - handleSignInOperation(idpConfig, loginButton); - } - //hide custom layout buttons that don't have their identity provider set - for (String providerBtnId : providerButtonIds.keySet()) { - if (providerBtnId == null) { - continue; - } - boolean hasProvider = false; - for (IdpConfig idpConfig : providerConfigs) { - String providerId = providerOrEmailLinkProvider(idpConfig.getProviderId()); - if (providerBtnId.equals(providerId)) { - hasProvider = true; - break; - } - } - if (!hasProvider) { - Integer resId = providerButtonIds.get(providerBtnId); - if (resId == null) { - continue; - } - @IdRes int buttonId = resId; - findViewById(buttonId).setVisibility(View.GONE); - } - } - } - - @NonNull - private String providerOrEmailLinkProvider(@NonNull String providerId) { - if (providerId.equals(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD)) { - return EmailAuthProvider.PROVIDER_ID; - } - - return providerId; - } - - private void handleSignInOperation(final IdpConfig idpConfig, View view) { - ViewModelProvider supplier = new ViewModelProvider(this); - final String providerId = idpConfig.getProviderId(); - final ProviderSignInBase provider; - - AuthUI authUI = getAuthUI(); - - switch (providerId) { - case EMAIL_LINK_PROVIDER: - case EmailAuthProvider.PROVIDER_ID: - provider = supplier.get(EmailSignInHandler.class).initWith(null); - break; - case PhoneAuthProvider.PROVIDER_ID: - provider = supplier.get(PhoneSignInHandler.class).initWith(idpConfig); - break; - case AuthUI.ANONYMOUS_PROVIDER: - provider = supplier.get(AnonymousSignInHandler.class).initWith(getFlowParams()); - break; - case GoogleAuthProvider.PROVIDER_ID: - if (authUI.isUseEmulator()) { - provider = supplier.get(GenericIdpSignInHandler.class) - .initWith(GenericIdpSignInHandler.getGenericGoogleConfig()); - } else { - provider = supplier.get(GoogleSignInHandler.class).initWith( - new GoogleSignInHandler.Params(idpConfig)); - } - break; - case FacebookAuthProvider.PROVIDER_ID: - if (authUI.isUseEmulator()) { - provider = supplier.get(GenericIdpSignInHandler.class) - .initWith(GenericIdpSignInHandler.getGenericFacebookConfig()); - } else { - provider = supplier.get(FacebookSignInHandler.class).initWith(idpConfig); - } - break; - default: - if (!TextUtils.isEmpty( - idpConfig.getParams().getString(GENERIC_OAUTH_PROVIDER_ID))) { - provider = supplier.get(GenericIdpSignInHandler.class).initWith(idpConfig); - break; - } - throw new IllegalStateException("Unknown provider: " + providerId); - } - - mProviders.add(provider); - - provider.getOperation().observe(this, new ResourceObserver(this) { - @Override - protected void onSuccess(@NonNull IdpResponse response) { - handleResponse(response); - } - - @Override - protected void onFailure(@NonNull Exception e) { - if (e instanceof FirebaseAuthAnonymousUpgradeException) { - finish(RESULT_CANCELED, new Intent().putExtra(ExtraConstants.IDP_RESPONSE, - IdpResponse.from(e))); - return; - } - handleResponse(IdpResponse.from(e)); - } - - private void handleResponse(@NonNull IdpResponse response) { - // If we're using the emulator then the social flows actually use Generic IDP - // instead which means we shouldn't use the social response handler. - boolean isSocialResponse = AuthUI.SOCIAL_PROVIDERS.contains(providerId) - && !getAuthUI().isUseEmulator(); - - if (!response.isSuccessful()) { - // We have no idea what provider this error stemmed from so just forward - // this along to the handler. - mHandler.startSignIn(response); - } else if (isSocialResponse) { - // Don't use the response's provider since it can be different than the one - // that launched the sign-in attempt. Ex: the email flow is started, but - // ends up turning into a Google sign-in because that account already - // existed. In the previous example, an extra sign-in would incorrectly - // started. - mHandler.startSignIn(response); - } else { - // Email, phone, or generic: the credentials should have already been saved so - // simply move along. - // Anononymous sign in also does not require any other operations. - finish(response.isSuccessful() ? RESULT_OK : RESULT_CANCELED, - response.toIntent()); - } - } - }); - view.setOnClickListener(view1 -> { - if (isOffline()) { - Snackbar.make(findViewById(android.R.id.content), getString(R.string.fui_no_internet), Snackbar.LENGTH_SHORT).show(); - return; - } - - provider.startSignIn(getAuth(), AuthMethodPickerActivity.this, - idpConfig.getProviderId()); - }); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - mHandler.onActivityResult(requestCode, resultCode, data); - for (ProviderSignInBase provider : mProviders) { - provider.onActivityResult(requestCode, resultCode, data); - } - } - - @Override - public void showProgress(int message) { - //mProgressBar & mProviderHolder might be null if using custom AuthMethodPickerLayout - if (customLayout == null) { - mProgressBar.setVisibility(View.VISIBLE); - for (int i = 0; i < mProviderHolder.getChildCount(); i++) { - View child = mProviderHolder.getChildAt(i); - child.setEnabled(false); - child.setAlpha(0.75f); - } - } - } - - @Override - public void hideProgress() { - //mProgressBar & mProviderHolder might be null if using custom AuthMethodPickerLayout - if (customLayout == null) { - mProgressBar.setVisibility(View.INVISIBLE); - for (int i = 0; i < mProviderHolder.getChildCount(); i++) { - View child = mProviderHolder.getChildAt(i); - child.setEnabled(true); - child.setAlpha(1.0f); - } - } - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.kt b/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.kt new file mode 100644 index 000000000..c2cc8ec32 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.kt @@ -0,0 +1,491 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.idp + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.firebase.ui.auth.AuthMethodPickerLayout +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.AuthUI.IdpConfig +import com.firebase.ui.auth.ErrorCodes +import com.firebase.ui.auth.FirebaseAuthAnonymousUpgradeException +import com.firebase.ui.auth.FirebaseUiException +import com.firebase.ui.auth.IdpResponse +import com.firebase.ui.auth.KickoffActivity +import com.firebase.ui.auth.R +import com.firebase.ui.auth.data.model.FlowParameters +import com.firebase.ui.auth.data.model.Resource +import com.firebase.ui.auth.data.model.User +import com.firebase.ui.auth.data.model.UserCancellationException +import com.firebase.ui.auth.data.remote.AnonymousSignInHandler +import com.firebase.ui.auth.data.remote.EmailSignInHandler +import com.firebase.ui.auth.data.remote.FacebookSignInHandler +import com.firebase.ui.auth.data.remote.GenericIdpSignInHandler +import com.firebase.ui.auth.data.remote.GoogleSignInHandler +import com.firebase.ui.auth.data.remote.PhoneSignInHandler +import com.firebase.ui.auth.ui.AppCompatBase +import com.firebase.ui.auth.util.ExtraConstants +import com.firebase.ui.auth.util.data.PrivacyDisclosureUtils +import com.firebase.ui.auth.util.data.ProviderUtils +import com.firebase.ui.auth.viewmodel.ProviderSignInBase +import com.firebase.ui.auth.viewmodel.ResourceObserver +import com.firebase.ui.auth.viewmodel.idp.SocialProviderResponseHandler +import com.google.android.gms.auth.api.identity.BeginSignInRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.auth.api.identity.SignInCredential +import com.google.android.gms.common.api.ApiException +import com.google.android.material.snackbar.Snackbar +import com.google.firebase.auth.EmailAuthProvider +import com.google.firebase.auth.FacebookAuthProvider +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException +import com.google.firebase.auth.FirebaseAuthInvalidUserException +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.PhoneAuthProvider +import kotlinx.coroutines.launch + +// Imports for the new Credential Manager types (adjust these to match your library) +import androidx.credentials.Credential +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetPasswordOption +import androidx.credentials.PasswordCredential +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.GetCredentialException + +import com.firebase.ui.auth.AuthUI.EMAIL_LINK_PROVIDER +import com.firebase.ui.auth.util.ExtraConstants.GENERIC_OAUTH_BUTTON_ID +import com.firebase.ui.auth.util.ExtraConstants.GENERIC_OAUTH_PROVIDER_ID +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException +import com.google.firebase.auth.GoogleAuthCredential + +@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) +class AuthMethodPickerActivity : AppCompatBase() { + + private lateinit var mHandler: SocialProviderResponseHandler + private val mProviders: MutableList> = mutableListOf() + + private var mProgressBar: ProgressBar? = null + private var mProviderHolder: ViewGroup? = null + + private var customLayout: AuthMethodPickerLayout? = null + + // For demonstration, assume that CredentialManager provides a create() method. + private val credentialManager by lazy { + // Replace with your actual CredentialManager instance creation. + CredentialManager.create(this) + } + + companion object { + private const val TAG = "AuthMethodPickerActivity" + + @JvmStatic + fun createIntent(context: Context, flowParams: FlowParameters): Intent { + return createBaseIntent(context, AuthMethodPickerActivity::class.java, flowParams) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val params = flowParams + customLayout = params.authMethodPickerLayout + + mHandler = ViewModelProvider(this).get(SocialProviderResponseHandler::class.java) + mHandler.init(params) + + if (customLayout != null) { + setContentView(customLayout!!.mainLayout) + populateIdpListCustomLayout(params.providers) + } else { + setContentView(R.layout.fui_auth_method_picker_layout) + mProgressBar = findViewById(R.id.top_progress_bar) + mProviderHolder = findViewById(R.id.btn_holder) + populateIdpList(params.providers) + + val logoId = params.logoId + if (logoId == AuthUI.NO_LOGO) { + findViewById(R.id.logo).visibility = View.GONE + + val layout = findViewById(R.id.root) + val constraints = ConstraintSet() + constraints.clone(layout) + constraints.setHorizontalBias(R.id.container, 0.5f) + constraints.setVerticalBias(R.id.container, 0.5f) + constraints.applyTo(layout) + } else { + val logo = findViewById(R.id.logo) + logo.setImageResource(logoId) + } + } + + val tosAndPpConfigured = flowParams.isPrivacyPolicyUrlProvided() && + flowParams.isTermsOfServiceUrlProvided() + + val termsTextId = if (customLayout == null) { + R.id.main_tos_and_pp + } else { + customLayout!!.tosPpView + } + + if (termsTextId >= 0) { + val termsText = findViewById(termsTextId) + if (!tosAndPpConfigured) { + termsText.visibility = View.GONE + } else { + PrivacyDisclosureUtils.setupTermsOfServiceAndPrivacyPolicyText(this, flowParams, termsText) + } + } + + // Observe the social provider response handler. + mHandler.operation.observe(this, object : ResourceObserver(this, R.string.fui_progress_dialog_signing_in) { + override fun onSuccess(response: IdpResponse) { + startSaveCredentials(mHandler.currentUser, response, null) + } + + override fun onFailure(e: Exception) { + when (e) { + is UserCancellationException -> { + // User pressed back – no error. + } + is FirebaseAuthAnonymousUpgradeException -> { + finish(ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT, e.response.toIntent()) + } + is FirebaseUiException -> { + finish(RESULT_CANCELED, IdpResponse.from(e).toIntent()) + } + else -> { + val text = getString(R.string.fui_error_unknown) + Toast.makeText(this@AuthMethodPickerActivity, text, Toast.LENGTH_SHORT).show() + } + } + } + }) + + // Attempt sign in using the new Credential Manager API. + attemptCredentialSignIn() + } + + /** + * Attempts to sign in automatically using the Credential Manager API. + */ + private fun attemptCredentialSignIn() { + val args = flowParams + val supportPasswords = ProviderUtils.getConfigFromIdps(args.providers, EmailAuthProvider.PROVIDER_ID) != null + val accountTypes = getCredentialAccountTypes() + val willRequestCredentials = supportPasswords || accountTypes.isNotEmpty() + + if (args.enableCredentials && willRequestCredentials) { + // Build the new Credential Manager request. + val getPasswordOption = GetPasswordOption() + val googleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(true) + .setServerClientId(getString(R.string.default_web_client_id)) + .build() + val request = GetCredentialRequest(listOf(getPasswordOption, googleIdOption)) + + lifecycleScope.launch { + try { + val result = credentialManager.getCredential( + context = this@AuthMethodPickerActivity, + request = request + ) + // Handle the returned credential. + handleCredentialManagerResult(result.credential) + } catch (e: GetCredentialException) { + handleCredentialManagerFailure(e) + // Fallback: show the auth method picker. + showAuthMethodPicker() + } + } + } else { + showAuthMethodPicker() + } + } + + /** + * Handles the credential returned from the Credential Manager. + */ + private fun handleCredentialManagerResult(credential: Credential) { + when (credential) { + is PasswordCredential -> { + val username = credential.id + val password = credential.password + val response = IdpResponse.Builder( + User.Builder(EmailAuthProvider.PROVIDER_ID, username).build() + ).build() + KickoffActivity.mKickstarter.setResult(Resource.forLoading()) + auth.signInWithEmailAndPassword(username, password) + .addOnSuccessListener { authResult -> + KickoffActivity.mKickstarter.handleSuccess(response, authResult) + finish() + } + .addOnFailureListener { e -> + if (e is FirebaseAuthInvalidUserException || + e is FirebaseAuthInvalidCredentialsException) { + // Sign out via the new API. + Identity.getSignInClient(application).signOut() + } + } + } + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + val googleIdTokenCredential = GoogleIdTokenCredential + .createFrom(credential.data) + auth.signInWithCredential(GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null)) + .addOnSuccessListener { authResult -> + val response = IdpResponse.Builder( + User.Builder(GoogleAuthProvider.PROVIDER_ID, googleIdTokenCredential.data.getString("email")).build(), + ).setToken(googleIdTokenCredential.idToken).build() + KickoffActivity.mKickstarter.handleSuccess(response, authResult) + finish() + } + .addOnFailureListener { e -> + Log.e(TAG, "Failed to sign in with Google ID token", e) + } + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) + } + } else { + // Catch any unrecognized custom credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + else -> { + Log.e(TAG, "Unexpected type of credential") + } + } + } + + /** + * Example helper to extract a Google ID token from a PublicKeyCredential. + * In your implementation you may need to parse the JSON response accordingly. + */ + private fun extractGoogleIdToken(credential: PublicKeyCredential): String? { + // TODO: Extract and return the Google ID token from credential.authenticationResponseJson. + // For demonstration, we assume that authenticationResponseJson is the token. + return credential.authenticationResponseJson + } + + private fun handleCredentialManagerFailure(e: GetCredentialException) { + Log.e(TAG, "Credential Manager sign in failed", e) + } + + /** + * Returns the account types to pass to the credential manager. + */ + private fun getCredentialAccountTypes(): List { + val accounts = mutableListOf() + for (idpConfig in flowParams.providers) { + if (idpConfig.providerId == GoogleAuthProvider.PROVIDER_ID) { + accounts.add(ProviderUtils.providerIdToAccountType(idpConfig.providerId)) + } + } + return accounts + } + + /** + * Fallback – show the auth method picker UI. + */ + private fun showAuthMethodPicker() { + hideProgress() + } + + private fun populateIdpList(providerConfigs: List) { + // Clear any previous providers. + mProviders.clear() + for (idpConfig in providerConfigs) { + val buttonLayout = when (idpConfig.providerId) { + GoogleAuthProvider.PROVIDER_ID -> R.layout.fui_idp_button_google + FacebookAuthProvider.PROVIDER_ID -> R.layout.fui_idp_button_facebook + EMAIL_LINK_PROVIDER, EmailAuthProvider.PROVIDER_ID -> R.layout.fui_provider_button_email + PhoneAuthProvider.PROVIDER_ID -> R.layout.fui_provider_button_phone + AuthUI.ANONYMOUS_PROVIDER -> R.layout.fui_provider_button_anonymous + else -> { + if (!TextUtils.isEmpty(idpConfig.params.getString(GENERIC_OAUTH_PROVIDER_ID))) { + idpConfig.params.getInt(GENERIC_OAUTH_BUTTON_ID) + } else { + throw IllegalStateException("Unknown provider: ${idpConfig.providerId}") + } + } + } + val loginButton = layoutInflater.inflate(buttonLayout, mProviderHolder, false) + handleSignInOperation(idpConfig, loginButton) + mProviderHolder?.addView(loginButton) + } + } + + private fun populateIdpListCustomLayout(providerConfigs: List) { + val providerButtonIds = customLayout?.providersButton ?: return + for (idpConfig in providerConfigs) { + val providerId = providerOrEmailLinkProvider(idpConfig.providerId) + val buttonResId = providerButtonIds[providerId] + ?: throw IllegalStateException("No button found for auth provider: ${idpConfig.providerId}") + val loginButton = findViewById(buttonResId) + handleSignInOperation(idpConfig, loginButton) + } + // Hide custom layout buttons that don't have an associated provider. + for ((providerBtnId, resId) in providerButtonIds) { + if (providerBtnId == null) continue + var hasProvider = false + for (idpConfig in providerConfigs) { + if (providerOrEmailLinkProvider(idpConfig.providerId) == providerBtnId) { + hasProvider = true + break + } + } + if (!hasProvider) { + findViewById(resId)?.visibility = View.GONE + } + } + } + + private fun providerOrEmailLinkProvider(providerId: String): String { + return if (providerId == EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD) { + EmailAuthProvider.PROVIDER_ID + } else providerId + } + + private fun handleSignInOperation(idpConfig: IdpConfig, view: View) { + val providerId = idpConfig.providerId + val authUI = getAuthUI() + val viewModelProvider = ViewModelProvider(this) + val provider: ProviderSignInBase<*> = when (providerId) { + EMAIL_LINK_PROVIDER, EmailAuthProvider.PROVIDER_ID -> + viewModelProvider.get(EmailSignInHandler::class.java).initWith(null) + PhoneAuthProvider.PROVIDER_ID -> + viewModelProvider.get(PhoneSignInHandler::class.java).initWith(idpConfig) + AuthUI.ANONYMOUS_PROVIDER -> + viewModelProvider.get(AnonymousSignInHandler::class.java).initWith(flowParams) + GoogleAuthProvider.PROVIDER_ID -> + if (authUI.isUseEmulator) { + viewModelProvider.get(GenericIdpSignInHandler::class.java) + .initWith(GenericIdpSignInHandler.getGenericGoogleConfig()) + } else { + viewModelProvider.get(GoogleSignInHandler::class.java) + .initWith(GoogleSignInHandler.Params(idpConfig)) + } + FacebookAuthProvider.PROVIDER_ID -> + if (authUI.isUseEmulator) { + viewModelProvider.get(GenericIdpSignInHandler::class.java) + .initWith(GenericIdpSignInHandler.getGenericFacebookConfig()) + } else { + viewModelProvider.get(FacebookSignInHandler::class.java).initWith(idpConfig) + } + else -> { + if (!TextUtils.isEmpty(idpConfig.params.getString(GENERIC_OAUTH_PROVIDER_ID))) { + viewModelProvider.get(GenericIdpSignInHandler::class.java).initWith(idpConfig) + } else { + throw IllegalStateException("Unknown provider: $providerId") + } + } + } + + mProviders.add(provider) + + provider.operation.observe(this, object : ResourceObserver(this) { + override fun onSuccess(response: IdpResponse) { + handleResponse(response) + } + + override fun onFailure(e: Exception) { + if (e is FirebaseAuthAnonymousUpgradeException) { + finish( + RESULT_CANCELED, + Intent().putExtra(ExtraConstants.IDP_RESPONSE, IdpResponse.from(e)) + ) + return + } + handleResponse(IdpResponse.from(e)) + } + + private fun handleResponse(response: IdpResponse) { + // For social providers (unless using an emulator) use the social response handler. + val isSocialResponse = AuthUI.SOCIAL_PROVIDERS.contains(providerId) && !authUI.isUseEmulator + if (!response.isSuccessful) { + mHandler.startSignIn(response) + } else if (isSocialResponse) { + mHandler.startSignIn(response) + } else { + finish(if (response.isSuccessful) RESULT_OK else RESULT_CANCELED, response.toIntent()) + } + } + }) + + view.setOnClickListener { + if (isOffline()) { + Snackbar.make(findViewById(android.R.id.content), getString(R.string.fui_no_internet), Snackbar.LENGTH_SHORT) + .show() + return@setOnClickListener + } + provider.startSignIn(getAuth(), this@AuthMethodPickerActivity, idpConfig.providerId) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + mHandler.onActivityResult(requestCode, resultCode, data) + for (provider in mProviders) { + provider.onActivityResult(requestCode, resultCode, data) + } + } + + override fun showProgress(message: Int) { + if (customLayout == null) { + mProgressBar?.visibility = View.VISIBLE + mProviderHolder?.let { holder -> + for (i in 0 until holder.childCount) { + val child = holder.getChildAt(i) + child.isEnabled = false + child.alpha = 0.75f + } + } + } + } + + override fun hideProgress() { + if (customLayout == null) { + mProgressBar?.visibility = View.INVISIBLE + mProviderHolder?.let { holder -> + for (i in 0 until holder.childCount) { + val child = holder.getChildAt(i) + child.isEnabled = true + child.alpha = 1.0f + } + } + } + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneHandler.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneHandler.java index 63a38b54d..c10e5067a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneHandler.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneHandler.java @@ -3,6 +3,8 @@ import android.app.Activity; import android.app.Application; import android.content.Intent; +import android.content.IntentSender; +import android.util.Log; import com.firebase.ui.auth.data.model.PendingIntentRequiredException; import com.firebase.ui.auth.data.model.PhoneNumber; @@ -10,35 +12,81 @@ import com.firebase.ui.auth.util.data.PhoneNumberUtils; import com.firebase.ui.auth.viewmodel.AuthViewModelBase; import com.firebase.ui.auth.viewmodel.RequestCodes; -import com.google.android.gms.auth.api.credentials.Credential; -import com.google.android.gms.auth.api.credentials.Credentials; -import com.google.android.gms.auth.api.credentials.HintRequest; +import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest; +import com.google.android.gms.auth.api.identity.Identity; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class CheckPhoneHandler extends AuthViewModelBase { + + private static final String TAG = "CheckPhoneHandler"; + public CheckPhoneHandler(Application application) { super(application); } - public void fetchCredential() { - setResult(Resource.forFailure(new PendingIntentRequiredException( - Credentials.getClient(getApplication()).getHintPickerIntent( - new HintRequest.Builder().setPhoneNumberIdentifierSupported(true).build()), - RequestCodes.CRED_HINT - ))); + /** + * Initiates the Phone Number Hint flow using the new API. + * + *

This method creates a GetPhoneNumberHintIntentRequest and calls + * Identity.getSignInClient(activity).getPhoneNumberHintIntent(request) to retrieve an + * IntentSender. The IntentSender is then wrapped in a PendingIntentRequiredException so that + * the caller can launch the hint flow. + * + *

Note: Update your PendingIntentRequiredException to accept an IntentSender + * rather than a PendingIntent. + * + * @param activity The activity used to retrieve the Phone Number Hint IntentSender. + */ + public void fetchCredential(final Activity activity) { + GetPhoneNumberHintIntentRequest request = GetPhoneNumberHintIntentRequest.builder().build(); + Identity.getSignInClient(activity) + .getPhoneNumberHintIntent(request) + .addOnSuccessListener(result -> { + try { + // The new API returns an IntentSender. + IntentSender intentSender = result.getIntentSender(); + // Update your exception to accept an IntentSender. + setResult(Resource.forFailure(new PendingIntentRequiredException(intentSender, RequestCodes.CRED_HINT))); + } catch (Exception e) { + Log.e(TAG, "Launching the IntentSender failed", e); + setResult(Resource.forFailure(e)); + } + }) + .addOnFailureListener(e -> { + Log.e(TAG, "Phone Number Hint failed", e); + setResult(Resource.forFailure(e)); + }); } - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) { return; } - - Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY); - String formattedPhone = PhoneNumberUtils.formatUsingCurrentCountry( - credential.getId(), getApplication()); - if (formattedPhone != null) { - setResult(Resource.forSuccess(PhoneNumberUtils.getPhoneNumber(formattedPhone))); + /** + * Handles the result from the Phone Number Hint flow. + * + *

Call this method from your Activity's onActivityResult. It extracts the phone number from the + * returned Intent and formats it. + * + * @param activity The activity used to process the returned Intent. + * @param requestCode The request code (should match RequestCodes.CRED_HINT). + * @param resultCode The result code from the hint flow. + * @param data The Intent data returned from the hint flow. + */ + public void onActivityResult(Activity activity, int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) { + return; + } + try { + String phoneNumber = Identity.getSignInClient(activity).getPhoneNumberFromIntent(data); + String formattedPhone = PhoneNumberUtils.formatUsingCurrentCountry(phoneNumber, getApplication()); + if (formattedPhone != null) { + setResult(Resource.forSuccess(PhoneNumberUtils.getPhoneNumber(formattedPhone))); + } else { + setResult(Resource.forFailure(new Exception("Failed to format phone number"))); + } + } catch (Exception e) { + Log.e(TAG, "Phone Number Hint failed", e); + setResult(Resource.forFailure(e)); } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java index a02e3057e..5dae792ad 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java @@ -30,9 +30,6 @@ import androidx.annotation.RestrictTo; import androidx.lifecycle.ViewModelProvider; -/** - * Displays country selector and phone number input form for users - */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class CheckPhoneNumberFragment extends FragmentBase implements View.OnClickListener { public static final String TAG = "VerifyPhoneFragment"; @@ -50,7 +47,6 @@ public class CheckPhoneNumberFragment extends FragmentBase implements View.OnCli private TextView mSmsTermsText; private TextView mFooterText; - public static CheckPhoneNumberFragment newInstance(Bundle params) { CheckPhoneNumberFragment fragment = new CheckPhoneNumberFragment(); Bundle args = new Bundle(); @@ -94,7 +90,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat } requireActivity().setTitle(getString(R.string.fui_verify_phone_number_title)); - ImeHelper.setImeOnDoneListener(mPhoneEditText, () -> onNext()); + ImeHelper.setImeOnDoneListener(mPhoneEditText, this::onNext); mSubmitButton.setOnClickListener(this); setupPrivacyDisclosures(); @@ -112,24 +108,24 @@ protected void onSuccess(@NonNull PhoneNumber number) { @Override protected void onFailure(@NonNull Exception e) { - // Just let the user enter their data + // Let the user enter their data if hint retrieval fails } }); if (savedInstanceState != null || mCalled) { return; } - // Fragment back stacks are the stuff of nightmares (what's new?): the fragment isn't - // destroyed so its state isn't saved and we have to rely on an instance field. Sigh. + // Fragment back stacks can cause state retention so we rely on an instance field. mCalled = true; - // DON'T REMOVE + // Set default country or prompt for phone number using the Phone Number Hint flow. setDefaultCountryForSpinner(); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - mCheckPhoneHandler.onActivityResult(requestCode, resultCode, data); + // Pass the activity instance to the handler + mCheckPhoneHandler.onActivityResult(requireActivity(), requestCode, resultCode, data); } @Override @@ -165,18 +161,15 @@ private void onNext() { @Nullable private String getPseudoValidPhoneNumber() { String everythingElse = mPhoneEditText.getText().toString(); - if (TextUtils.isEmpty(everythingElse)) { return null; } - return PhoneNumberUtils.format( everythingElse, mCountryListSpinner.getSelectedCountryInfo()); } private void setupPrivacyDisclosures() { FlowParameters params = getFlowParams(); - boolean termsAndPrivacyUrlsProvided = params.isTermsOfServiceUrlProvided() && params.isPrivacyPolicyUrlProvided(); @@ -188,7 +181,6 @@ private void setupPrivacyDisclosures() { PrivacyDisclosureUtils.setupTermsOfServiceFooter(requireContext(), params, mFooterText); - String verifyText = getString(R.string.fui_verify_phone_number); mSmsTermsText.setText(getString(R.string.fui_sms_terms_of_service, verifyText)); } @@ -202,15 +194,12 @@ private void setCountryCode(PhoneNumber number) { private void setupCountrySpinner() { Bundle params = getArguments().getBundle(ExtraConstants.PARAMS); mCountryListSpinner.init(params, mCountryListAnchor); - - // Clear error when spinner is clicked on + // Clear error when spinner is clicked mCountryListSpinner.setOnClickListener(v -> mPhoneInputLayout.setError(null)); } private void setDefaultCountryForSpinner() { - // Check for phone - // It is assumed that the phone number that are being wired in via Credential Selector - // are e164 since we store it. + // Check for phone number defaults Bundle params = getArguments().getBundle(ExtraConstants.PARAMS); String phone = null; String countryIso = null; @@ -221,10 +210,7 @@ private void setDefaultCountryForSpinner() { nationalNumber = params.getString(ExtraConstants.NATIONAL_NUMBER); } - // We can receive the phone number in one of two formats: split between the ISO or fully - // processed. If it's complete, we use it directly. Otherwise, we parse the ISO and national - // number combination or we just set the default ISO if there's no default number. If there - // are no defaults at all, we prompt the user for a phone number through Smart Lock. + // If phone is provided in full, use it. Otherwise, parse ISO and national number or prompt for a phone hint. if (!TextUtils.isEmpty(phone)) { start(PhoneNumberUtils.getPhoneNumber(phone)); } else if (!TextUtils.isEmpty(countryIso) && !TextUtils.isEmpty(nationalNumber)) { @@ -235,7 +221,8 @@ private void setDefaultCountryForSpinner() { countryIso, String.valueOf(PhoneNumberUtils.getCountryCode(countryIso)))); } else if (getFlowParams().enableHints) { - mCheckPhoneHandler.fetchCredential(); + // Launch phone number hint flow using the new API + mCheckPhoneHandler.fetchCredential(requireActivity()); } } diff --git a/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java b/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java index df318ad64..ca21a9553 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java @@ -4,8 +4,6 @@ import android.text.TextUtils; import android.util.Log; -import com.firebase.ui.auth.IdpResponse; -import com.google.android.gms.auth.api.credentials.Credential; import com.google.firebase.auth.FirebaseUser; import androidx.annotation.NonNull; @@ -13,10 +11,10 @@ import androidx.annotation.RestrictTo; /** - * Utility class for working with {@link Credential} objects. + * Utility class for extracting credential data from a {@link FirebaseUser} for the new CredentialManager. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class CredentialUtils { +public final class CredentialUtils { private static final String TAG = "CredentialUtils"; @@ -25,54 +23,85 @@ private CredentialUtils() { } /** - * Build a credential for the specified {@link FirebaseUser} with optional password and {@link - * IdpResponse}. + * Extracts the necessary data from the specified {@link FirebaseUser} along with the user's password. *

- * If the credential cannot be built (for example, empty email) then will return {@code null}. + * If both the email and phone number are missing or the password is empty, this method returns {@code null}. + * + * @param user the FirebaseUser from which to extract data. + * @param password the password the user signed in with. + * @return a {@link CredentialData} instance containing the user’s sign-in information, or {@code null} if insufficient data. */ @Nullable - public static Credential buildCredential(@NonNull FirebaseUser user, - @Nullable String password, - @Nullable String accountType) { + public static CredentialData buildCredentialData(@NonNull FirebaseUser user, + @Nullable String password) { String email = user.getEmail(); String phone = user.getPhoneNumber(); - Uri profilePictureUri = - user.getPhotoUrl() == null ? null : Uri.parse(user.getPhotoUrl().toString()); + Uri profilePictureUri = (user.getPhotoUrl() != null) + ? Uri.parse(user.getPhotoUrl().toString()) + : null; if (TextUtils.isEmpty(email) && TextUtils.isEmpty(phone)) { - Log.w(TAG, "User (accountType=" + accountType + ") has no email or phone number, cannot build credential."); + Log.w(TAG, "User has no email or phone number; cannot build credential data."); return null; } - if (password == null && accountType == null) { - Log.w(TAG, "User has no accountType or password, cannot build credential."); + if (TextUtils.isEmpty(password)) { + Log.w(TAG, "Password is required to build credential data."); return null; } - Credential.Builder builder = - new Credential.Builder(TextUtils.isEmpty(email) ? phone : email) - .setName(user.getDisplayName()) - .setProfilePictureUri(profilePictureUri); + // Prefer email if available; otherwise fall back to phone. + String identifier = !TextUtils.isEmpty(email) ? email : phone; + return new CredentialData(identifier, user.getDisplayName(), password, profilePictureUri); + } - if (TextUtils.isEmpty(password)) { - builder.setAccountType(accountType); - } else { - builder.setPassword(password); + /** + * Same as {@link #buildCredentialData(FirebaseUser, String)} but throws an exception if data cannot be built. + * + * @param user the FirebaseUser. + * @param password the password the user signed in with. + * @return a non-null {@link CredentialData} instance. + * @throws IllegalStateException if credential data cannot be constructed. + */ + @NonNull + public static CredentialData buildCredentialDataOrThrow(@NonNull FirebaseUser user, + @Nullable String password) { + CredentialData credentialData = buildCredentialData(user, password); + if (credentialData == null) { + throw new IllegalStateException("Unable to build credential data"); } - - return builder.build(); + return credentialData; } /** - * @see #buildCredential(FirebaseUser, String, String) + * A simple data class representing the information required by the new CredentialManager. */ - @NonNull - public static Credential buildCredentialOrThrow(@NonNull FirebaseUser user, - @Nullable String password, - @Nullable String accountType) { - Credential credential = buildCredential(user, password, accountType); - if (credential == null) { - throw new IllegalStateException("Unable to build credential"); + public static final class CredentialData { + private final String identifier; + private final String displayName; + private final String password; + private final Uri profilePictureUri; + + public CredentialData(String identifier, String displayName, String password, Uri profilePictureUri) { + this.identifier = identifier; + this.displayName = displayName; + this.password = password; + this.profilePictureUri = profilePictureUri; + } + + public String getIdentifier() { + return identifier; + } + + public String getDisplayName() { + return displayName; + } + + public String getPassword() { + return password; + } + + public Uri getProfilePictureUri() { + return profilePictureUri; } - return credential; } } diff --git a/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java b/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java index 4dd780128..07211a4ea 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java @@ -27,6 +27,7 @@ public final class ExtraConstants { public static final String CREDENTIAL = "extra_credential"; public static final String EMAIL = "extra_email"; + public static final String PASSWORD = "extra_password"; public static final String DEFAULT_EMAIL = "extra_default_email"; public static final String ALLOW_NEW_EMAILS = "extra_allow_new_emails"; public static final String REQUIRE_NAME = "extra_require_name"; diff --git a/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java b/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java index e1d3d8681..65fe147ef 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java @@ -22,7 +22,6 @@ import com.firebase.ui.auth.IdpResponse; import com.firebase.ui.auth.R; import com.firebase.ui.auth.data.model.FlowParameters; -import com.google.android.gms.auth.api.credentials.IdentityProviders; import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; @@ -47,6 +46,9 @@ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public final class ProviderUtils { + private static final String GOOGLE_ACCOUNT_TYPE = "https://accounts.google.com"; + private static final String FACEBOOK_ACCOUNT_TYPE = "https://www.facebook.com"; + private static final String TWITTER_ACCOUNT_TYPE = "https://twitter.com"; private static final String GITHUB_IDENTITY = "https://github.com"; private static final String PHONE_IDENTITY = "https://phone.firebase"; @@ -74,7 +76,6 @@ public static String idpResponseToAccountType(@Nullable IdpResponse response) { if (response == null) { return null; } - return providerIdToAccountType(response.getProviderType()); } @@ -103,22 +104,22 @@ public static String signInMethodToProviderId(@NonNull String method) { /** * Translate a Firebase Auth provider ID (such as {@link GoogleAuthProvider#PROVIDER_ID}) to a - * Credentials API account type (such as {@link IdentityProviders#GOOGLE}). + * Credentials API account type. */ public static String providerIdToAccountType( @AuthUI.SupportedProvider @NonNull String providerId) { switch (providerId) { case GoogleAuthProvider.PROVIDER_ID: - return IdentityProviders.GOOGLE; + return GOOGLE_ACCOUNT_TYPE; case FacebookAuthProvider.PROVIDER_ID: - return IdentityProviders.FACEBOOK; + return FACEBOOK_ACCOUNT_TYPE; case TwitterAuthProvider.PROVIDER_ID: - return IdentityProviders.TWITTER; + return TWITTER_ACCOUNT_TYPE; case GithubAuthProvider.PROVIDER_ID: return GITHUB_IDENTITY; case PhoneAuthProvider.PROVIDER_ID: return PHONE_IDENTITY; - // The account type for email/password creds is null + // The account type for email/password creds is null. case EmailAuthProvider.PROVIDER_ID: default: return null; @@ -128,11 +129,11 @@ public static String providerIdToAccountType( @AuthUI.SupportedProvider public static String accountTypeToProviderId(@NonNull String accountType) { switch (accountType) { - case IdentityProviders.GOOGLE: + case GOOGLE_ACCOUNT_TYPE: return GoogleAuthProvider.PROVIDER_ID; - case IdentityProviders.FACEBOOK: + case FACEBOOK_ACCOUNT_TYPE: return FacebookAuthProvider.PROVIDER_ID; - case IdentityProviders.TWITTER: + case TWITTER_ACCOUNT_TYPE: return TwitterAuthProvider.PROVIDER_ID; case GITHUB_IDENTITY: return GithubAuthProvider.PROVIDER_ID; @@ -215,7 +216,7 @@ public Task> then(@NonNull Task task) { // In this case the developer has configured EMAIL_LINK sign in but the // user is a password user. The valid use case here is that the developer // is using admin-created accounts and combining email-link sign in with - // setAllowNewAccounts(false). So we manually enable EMAIL_LINK. See: + // setAllowNewAccounts(false). So we manually enable EMAIL_LINK. See: // https://github.com/firebase/FirebaseUI-Android/issues/1762#issuecomment-661115293 if (allowedProviders.contains(EMAIL_LINK_PROVIDER) && methods.contains(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) @@ -226,8 +227,7 @@ public Task> then(@NonNull Task task) { if (task.isSuccessful() && lastSignedInProviders.isEmpty() && !methods.isEmpty()) { // There is an existing user who only has unsupported sign in methods - return Tasks.forException(new FirebaseUiException(ErrorCodes - .DEVELOPER_ERROR)); + return Tasks.forException(new FirebaseUiException(ErrorCodes.DEVELOPER_ERROR)); } // Reorder providers from most to least usable. Usability is determined by // how many steps a user needs to perform to log in. @@ -239,7 +239,7 @@ public Task> then(@NonNull Task task) { private void reorderPriorities(List providers) { // Prioritize Google over everything else // Prioritize email-password sign in second - // De-prioritize email link sign in + // De-prioritize email link sign in changePriority(providers, EmailAuthProvider.PROVIDER_ID, true); changePriority(providers, GoogleAuthProvider.PROVIDER_ID, true); changePriority(providers, EMAIL_LINK_PROVIDER, false); diff --git a/auth/src/main/java/com/firebase/ui/auth/viewmodel/credentialmanager/CredentialManagerHandler.kt b/auth/src/main/java/com/firebase/ui/auth/viewmodel/credentialmanager/CredentialManagerHandler.kt index db15e56d0..2c75e47e2 100644 --- a/auth/src/main/java/com/firebase/ui/auth/viewmodel/credentialmanager/CredentialManagerHandler.kt +++ b/auth/src/main/java/com/firebase/ui/auth/viewmodel/credentialmanager/CredentialManagerHandler.kt @@ -28,10 +28,16 @@ class CredentialManagerHandler(application: Application) : /** * Saves credentials via Credential Manager if enabled in [getArguments().enableCredentials]. * Uses a password-based credential for demonstration; adapt to passkeys or other flows as needed. + * + * @param context the Context to use. + * @param firebaseUser the current FirebaseUser. + * @param email the email to use as the identifier. + * @param password the password used for sign-in. */ fun saveCredentials( context: Context, firebaseUser: FirebaseUser?, + email: String?, password: String? ) { if (!arguments.enableCredentials) { @@ -40,31 +46,31 @@ class CredentialManagerHandler(application: Application) : } setResult(Resource.forLoading()) - if (firebaseUser == null || firebaseUser.email.isNullOrEmpty() || password.isNullOrEmpty()) { + if (firebaseUser == null || email.isNullOrEmpty() || password.isNullOrEmpty()) { setResult( Resource.forFailure( FirebaseUiException( ErrorCodes.UNKNOWN_ERROR, - "Invalid FirebaseUser or missing password." + "Invalid FirebaseUser or missing email/password." ) ) ) return } - // Example: Password credential with the user's email as the identifier + // Create a password-based credential using the provided email as the identifier. val request = CreatePasswordRequest( - id = firebaseUser.email!!, + id = email, password = password ) viewModelScope.launch { try { - // Use the createCredential function and store the response + // Use the CredentialManager to create (i.e., save) the credential. val createResponse: CreateCredentialResponse = credentialManager.createCredential(context, request) - // If the response is successful, set the success result + // If successful, report success. if (createResponse != null) { setResult(Resource.forSuccess(response!!)) } else { diff --git a/auth/src/main/res/layout/fui_check_email_layout.xml b/auth/src/main/res/layout/fui_check_email_layout.xml index f0416662e..d0d4fb8c8 100644 --- a/auth/src/main/res/layout/fui_check_email_layout.xml +++ b/auth/src/main/res/layout/fui_check_email_layout.xml @@ -48,11 +48,22 @@ + +