diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index c51fd3fd3..2eddf8176 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -8,13 +8,13 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: set up JDK 11
- uses: actions/setup-java@v1
- with:
- java-version: 11
- - name: Build with Gradle
- run: ./scripts/build.sh
- - name: Print Logs
- if: failure()
- run: ./scripts/print_build_logs.sh
\ No newline at end of file
+ - uses: actions/checkout@v2
+ - name: set up JDK 17
+ uses: actions/setup-java@v1
+ with:
+ java-version: 17
+ - name: Build with Gradle
+ run: ./scripts/build.sh
+ - name: Print Logs
+ if: failure()
+ run: ./scripts/print_build_logs.sh
diff --git a/README.md b/README.md
index 23c773544..81c2b6c88 100644
--- a/README.md
+++ b/README.md
@@ -48,16 +48,16 @@ libraries.
```groovy
dependencies {
// FirebaseUI for Firebase Realtime Database
- implementation 'com.firebaseui:firebase-ui-database:8.0.2'
+ implementation 'com.firebaseui:firebase-ui-database:9.0.0'
// FirebaseUI for Cloud Firestore
- implementation 'com.firebaseui:firebase-ui-firestore:8.0.2'
+ implementation 'com.firebaseui:firebase-ui-firestore:9.0.0'
// FirebaseUI for Firebase Auth
- implementation 'com.firebaseui:firebase-ui-auth:8.0.2'
+ implementation 'com.firebaseui:firebase-ui-auth:9.0.0'
// FirebaseUI for Cloud Storage
- implementation 'com.firebaseui:firebase-ui-storage:8.0.2'
+ implementation 'com.firebaseui:firebase-ui-storage:9.0.0'
}
```
@@ -71,6 +71,7 @@ After the project is synchronized, we're ready to start using Firebase functiona
If you are using an old version of FirebaseUI and upgrading, please see the appropriate
migration guide:
+* [Upgrade from 8.0.2 to 9.x.x](./docs/upgrade-to-9.0.md)
* [Upgrade from 7.2.0 to 8.x.x](./docs/upgrade-to-8.0.md)
* [Upgrade from 6.4.0 to 7.x.x](./docs/upgrade-to-7.0.md)
* [Upgrade from 5.1.0 to 6.x.x](./docs/upgrade-to-6.0.md)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fc2c58cea..cec75d966 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -7,6 +7,8 @@ plugins {
android {
compileSdk = Config.SdkVersions.compile
+ namespace = "com.firebase.uidemo"
+
defaultConfig {
minSdk = Config.SdkVersions.min
targetSdk = Config.SdkVersions.target
@@ -60,8 +62,8 @@ android {
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
@@ -95,9 +97,6 @@ dependencies {
implementation(Config.Libs.Misc.permissions)
implementation(Config.Libs.Androidx.constraint)
debugImplementation(Config.Libs.Misc.leakCanary)
- debugImplementation(Config.Libs.Misc.leakCanaryFragments)
- releaseImplementation(Config.Libs.Misc.leakCanaryNoop)
- testImplementation(Config.Libs.Misc.leakCanaryNoop)
}
apply(plugin = "com.google.gms.google-services")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 36dc8f03b..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/app/src/main/java/com/firebase/uidemo/FirebaseUIDemo.java b/app/src/main/java/com/firebase/uidemo/FirebaseUIDemo.java
index 687dcd564..db33245f8 100644
--- a/app/src/main/java/com/firebase/uidemo/FirebaseUIDemo.java
+++ b/app/src/main/java/com/firebase/uidemo/FirebaseUIDemo.java
@@ -1,7 +1,5 @@
package com.firebase.uidemo;
-import com.squareup.leakcanary.LeakCanary;
-
import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;
@@ -9,15 +7,4 @@ public class FirebaseUIDemo extends MultiDexApplication {
static {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
}
-
- @Override
- public void onCreate() {
- super.onCreate();
- if (LeakCanary.isInAnalyzerProcess(this)) {
- // This process is dedicated to LeakCanary for heap analysis.
- // You should not init your app in this process.
- return;
- }
- LeakCanary.install(this);
- }
}
diff --git a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java
index f3e8ef5c0..0a8d44cd3 100644
--- a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java
+++ b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java
@@ -141,8 +141,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
mBinding.signIn.setOnClickListener(view -> signIn());
- mBinding.signInSilent.setOnClickListener(view -> silentSignIn());
-
if (ConfigurationUtils.isGoogleMisconfigured(this)
|| ConfigurationUtils.isFacebookMisconfigured(this)) {
showSnackbar(R.string.configuration_required);
@@ -196,8 +194,7 @@ private Intent getSignInIntent(@Nullable String link) {
.setTheme(getSelectedTheme())
.setLogo(getSelectedLogo())
.setAvailableProviders(getSelectedProviders())
- .setIsSmartLockEnabled(mBinding.credentialSelectorEnabled.isChecked(),
- mBinding.hintSelectorEnabled.isChecked());
+ .setCredentialManagerEnabled(mBinding.credentialSelectorEnabled.isChecked());
if (mBinding.customLayout.isChecked()) {
AuthMethodPickerLayout customLayout = new AuthMethodPickerLayout
@@ -226,18 +223,9 @@ private Intent getSignInIntent(@Nullable String link) {
if (auth.getCurrentUser() != null && auth.getCurrentUser().isAnonymous()) {
builder.enableAnonymousUsersAutoUpgrade();
}
- return builder.build();
- }
- public void silentSignIn() {
- getAuthUI().silentSignIn(this, getSelectedProviders())
- .addOnCompleteListener(this, task -> {
- if (task.isSuccessful()) {
- startSignedInActivity(null);
- } else {
- showSnackbar(R.string.sign_in_failed);
- }
- });
+ builder.setAlwaysShowSignInMethodScreen(true);
+ return builder.build();
}
@Override
diff --git a/app/src/main/res/layout/auth_ui_layout.xml b/app/src/main/res/layout/auth_ui_layout.xml
index e3b02008d..a22375abb 100644
--- a/app/src/main/res/layout/auth_ui_layout.xml
+++ b/app/src/main/res/layout/auth_ui_layout.xml
@@ -1,5 +1,6 @@
-
+ app:drawableTopCompat="@drawable/firebase_auth_120dp" />
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9937ba5cd..88d0ed1d8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -63,8 +63,7 @@
PhotosOther Options
- Enable Smart Lock\'s credential selector
- Enable Smart Lock\'s hint selector
+ Enable Credential Manager\'s credential selectorAllow new account creationRequire first/last name with email accounts.Connect to auth emulator (localhost:9099).
diff --git a/auth/README.md b/auth/README.md
index 94553e956..b705b30d0 100644
--- a/auth/README.md
+++ b/auth/README.md
@@ -13,7 +13,7 @@ providers such as Google Sign-In, and Facebook Login. It is built on top of
The best practices embodied in FirebaseUI aim to maximize sign-in
and sign-up conversion for your app. It integrates with
-[Smart Lock for Passwords](https://developers.google.com/identity/smartlock-passwords/android/)
+[Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)
to store and retrieve credentials, enabling automatic and single-tap sign-in to
your app for returning users. It also handles tricky use cases like
account recovery and account linking that are security sensitive and
@@ -38,7 +38,6 @@ and [Web](https://github.com/firebase/firebaseui-web/).
1. [Usage instructions](#using-firebaseui-for-authentication)
1. [AuthUI sign-in](#authui-sign-in)
1. [Handling responses](#handling-the-sign-in-response)
- 1. [Silent sign-in](#silent-sign-in)
1. [Sign out](#sign-out)
1. [Account deletion](#deleting-accounts)
1. [Upgrading Anonymous Users](#upgrading-anonymous-users)
@@ -65,7 +64,7 @@ Gradle, add the dependency:
```groovy
dependencies {
// ...
- implementation 'com.firebaseui:firebase-ui-auth:8.0.2'
+ implementation 'com.firebaseui:firebase-ui-auth:9.0.0'
// Required only if Facebook login support is required
// Find the latest Facebook SDK releases here: https://github.com/facebook/facebook-android-sdk/blob/master/CHANGELOG.md
@@ -406,45 +405,19 @@ Intent signInIntent =
.build();
```
-##### Smart Lock
+##### Credential Manager
-By default, FirebaseUI uses [Smart Lock for Passwords](https://developers.google.com/identity/smartlock-passwords/android/)
+By default, FirebaseUI uses [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)
to store the user's credentials and automatically sign users into your app on subsequent attempts.
-Using Smart Lock is recommended to provide the best user experience, but in some cases you may want
-to disable Smart Lock for testing or development. To disable Smart Lock, you can use the
-`setIsSmartLockEnabled` method when building your sign-in Intent:
+Using Credential Manager is recommended to provide the best user experience, but in some cases you may want
+to disable Credential Manager for testing or development. To disable Credential Manager, you can use the
+`setCredentialManagerEnabled` method when building your sign-in Intent:
```java
Intent signInIntent =
AuthUI.getInstance()
.createSignInIntentBuilder()
- .setIsSmartLockEnabled(false)
- .build();
-```
-
-###### Smart Lock hints
-
-If you'd like to keep Smart Lock's "hints" but disable the saving/retrieving of credentials, then
-you can use the two-argument version of `setIsSmartLockEnabled`:
-
-```java
-Intent signInIntent =
- AuthUI.getInstance()
- .createSignInIntentBuilder()
- .setIsSmartLockEnabled(false, true)
- .build();
-```
-
-###### Smart Lock in dev builds
-
-It is often desirable to disable Smart Lock in development but enable it in production. To achieve
-this, you can use the `BuildConfig.DEBUG` flag to control Smart Lock:
-
-```java
-Intent signInIntent =
- AuthUI.getInstance()
- .createSignInIntentBuilder()
- .setIsSmartLockEnabled(!BuildConfig.DEBUG /* credentials */, true /* hints */)
+ .setCredentialManagerEnabled(false)
.build();
```
@@ -603,48 +576,13 @@ if (metadata.getCreationTimestamp() == metadata.getLastSignInTimestamp()) {
}
```
-### Silent sign-in
-
-If a user is not currently signed in, then a silent sign-in process can be started first before
-displaying any UI to provide a seamless experience. Silent sign-in uses saved Smart Lock credentials
-and returns a successful `Task` only if the user has been fully signed in with Firebase.
-
-Here's an example of how you could use silent sign-in paired with Firebase anonymous sign-in to get
-your users up and running as fast as possible:
-
-```java
-List providers = getSelectedProviders();
-AuthUI.getInstance().silentSignIn(this, providers)
- .continueWithTask(this, new Continuation>() {
- @Override
- public Task then(@NonNull Task task) {
- if (task.isSuccessful()) {
- return task;
- } else {
- // Ignore any exceptions since we don't care about credential fetch errors.
- return FirebaseAuth.getInstance().signInAnonymously();
- }
- }
-}).addOnCompleteListener(this, new OnCompleteListener() {
- @Override
- public void onComplete(@NonNull Task task) {
- if (task.isSuccessful()) {
- // Signed in! Start loading data
- } else {
- // Uh oh, show error message
- }
- }
-});
-```
-
### Sign out
With the integrations provided by AuthUI, signing out a user is a multi-stage process:
1. The user must be signed out of the FirebaseAuth instance.
-1. Smart Lock for Passwords must be instructed to disable automatic sign-in, in
- order to prevent an automatic sign-in loop that prevents the user from
- switching accounts.
+1. Credential Manager must be instructed to clear the current user credential state from
+ all credential providers.
1. If the current user signed in using either Google or Facebook, the user must
also be signed out using the associated API for that authentication method.
This typically ensures that the user will not be automatically signed-in
@@ -677,7 +615,7 @@ if (v.getId() == R.id.sign_out) {
With the integrations provided by FirebaseUI Auth, deleting a user is a multi-stage process:
1. The user must be deleted from Firebase Auth.
-1. Smart Lock for Passwords must be told to delete any existing Credentials for the user, so
+1. Credential Manager must be told to delete any existing Credentials for the user, so
that they are not automatically prompted to sign in with a saved credential in the future.
This process is encapsulated by the `AuthUI.delete()` method, which returns a `Task` representing
diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts
index 2a0ed1518..66df263a8 100644
--- a/auth/build.gradle.kts
+++ b/auth/build.gradle.kts
@@ -1,12 +1,14 @@
import com.android.build.gradle.internal.dsl.TestOptions
plugins {
- id("com.android.library")
- id("com.vanniktech.maven.publish")
+ id("com.android.library")
+ id("com.vanniktech.maven.publish")
+ id("org.jetbrains.kotlin.android")
}
android {
compileSdk = Config.SdkVersions.compile
+ namespace = "com.firebase.ui.auth"
defaultConfig {
minSdk = Config.SdkVersions.min
@@ -26,8 +28,8 @@ android {
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
lint {
@@ -62,6 +64,9 @@ android {
isIncludeAndroidResources = true
}
}
+ kotlinOptions {
+ jvmTarget = "17"
+ }
}
dependencies {
@@ -72,8 +77,15 @@ dependencies {
implementation(Config.Libs.Androidx.fragment)
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 5aee51352..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/AuthUI.java b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java
index 331c38a9c..b1ac3afbd 100644
--- a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java
+++ b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java
@@ -17,47 +17,36 @@
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.CancellationSignal;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.TextUtils;
import android.util.Log;
import com.facebook.login.LoginManager;
import com.firebase.ui.auth.data.model.FlowParameters;
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.GoogleApiUtils;
import com.firebase.ui.auth.util.Preconditions;
import com.firebase.ui.auth.util.data.PhoneNumberUtils;
import com.firebase.ui.auth.util.data.ProviderAvailability;
-import com.firebase.ui.auth.util.data.ProviderUtils;
-import com.google.android.gms.auth.api.credentials.Credential;
-import com.google.android.gms.auth.api.credentials.CredentialRequest;
-import com.google.android.gms.auth.api.credentials.CredentialsClient;
-import com.google.android.gms.auth.api.signin.GoogleSignIn;
-import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
-import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.ActionCodeSettings;
-import com.google.firebase.auth.AuthCredential;
-import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.EmailAuthProvider;
import com.google.firebase.auth.FacebookAuthProvider;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthInvalidUserException;
-import com.google.firebase.auth.FirebaseAuthProvider;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GithubAuthProvider;
import com.google.firebase.auth.GoogleAuthProvider;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.auth.TwitterAuthProvider;
-import com.google.firebase.auth.UserInfo;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -71,6 +60,8 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import androidx.annotation.CallSuper;
import androidx.annotation.DrawableRes;
@@ -79,6 +70,9 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
import androidx.annotation.StyleRes;
+import androidx.credentials.ClearCredentialStateRequest;
+import androidx.credentials.CredentialManagerCallback;
+import androidx.credentials.exceptions.ClearCredentialException;
/**
* The entry point to the AuthUI authentication flow, and related utility methods. If your
@@ -269,115 +263,6 @@ public static int getDefaultTheme() {
return R.style.FirebaseUI_DefaultMaterialTheme;
}
- /**
- * Make a list of {@link Credential} from a FirebaseUser. Useful for deleting Credentials, not
- * for saving since we don't have access to the password.
- */
- private static List getCredentialsFromFirebaseUser(@NonNull FirebaseUser user) {
- if (TextUtils.isEmpty(user.getEmail()) && TextUtils.isEmpty(user.getPhoneNumber())) {
- return Collections.emptyList();
- }
-
- List credentials = new ArrayList<>();
- for (UserInfo userInfo : user.getProviderData()) {
- if (FirebaseAuthProvider.PROVIDER_ID.equals(userInfo.getProviderId())) {
- continue;
- }
-
- String type = ProviderUtils.providerIdToAccountType(userInfo.getProviderId());
- if (type == null) {
- // Since the account type is null, we've got an email credential. Adding a fake
- // password is the only way to tell Smart Lock that this is an email credential.
- credentials.add(CredentialUtils.buildCredentialOrThrow(user, "pass", null));
- } else {
- credentials.add(CredentialUtils.buildCredentialOrThrow(user, null, type));
- }
- }
-
- return credentials;
- }
-
- /**
- * Signs the user in without any UI if possible. If this operation fails, you can safely start a
- * UI-based sign-in flow knowing it is required.
- *
- * @param context requesting the user be signed in
- * @param configs to use for silent sign in. Only Google and email are currently supported, the
- * rest will be ignored.
- * @return a task which indicates whether or not the user was successfully signed in.
- */
- @NonNull
- public Task silentSignIn(@NonNull Context context,
- @NonNull List configs) {
- if (mAuth.getCurrentUser() != null) {
- throw new IllegalArgumentException("User already signed in!");
- }
-
- final Context appContext = context.getApplicationContext();
- final IdpConfig google =
- ProviderUtils.getConfigFromIdps(configs, GoogleAuthProvider.PROVIDER_ID);
- final IdpConfig email =
- ProviderUtils.getConfigFromIdps(configs, EmailAuthProvider.PROVIDER_ID);
-
- if (google == null && email == null) {
- throw new IllegalArgumentException("No supported providers were supplied. " +
- "Add either Google or email support.");
- }
-
- final GoogleSignInOptions googleOptions;
- if (google == null) {
- googleOptions = null;
- } else {
- GoogleSignInAccount last = GoogleSignIn.getLastSignedInAccount(appContext);
- if (last != null && last.getIdToken() != null) {
- return mAuth.signInWithCredential(GoogleAuthProvider.getCredential(
- last.getIdToken(), null));
- }
-
- googleOptions = google.getParams()
- .getParcelable(ExtraConstants.GOOGLE_SIGN_IN_OPTIONS);
- }
-
- // If Play services are not available we can't attempt to use the credentials client.
- if (!GoogleApiUtils.isPlayServicesAvailable(context)) {
- return Tasks.forException(
- new FirebaseUiException(ErrorCodes.PLAY_SERVICES_UPDATE_CANCELLED));
- }
-
- return GoogleApiUtils.getCredentialsClient(context)
- .request(new CredentialRequest.Builder()
- // We can support both email and Google at the same time here because they
- // are mutually exclusive. If a user signs in with Google, their email
- // account will automatically be upgraded (a.k.a. replaced) with the Google
- // one, meaning Smart Lock won't have to show the picker UI.
- .setPasswordLoginSupported(email != null)
- .setAccountTypes(google == null ? null :
- ProviderUtils.providerIdToAccountType(GoogleAuthProvider
- .PROVIDER_ID))
- .build())
- .continueWithTask(task -> {
- Credential credential = task.getResult().getCredential();
- String email1 = credential.getId();
- String password = credential.getPassword();
-
- if (TextUtils.isEmpty(password)) {
- return GoogleSignIn.getClient(appContext,
- new GoogleSignInOptions.Builder(googleOptions)
- .setAccountName(email1)
- .build())
- .silentSignIn()
- .continueWithTask(task1 -> {
- AuthCredential authCredential = GoogleAuthProvider
- .getCredential(
- task1.getResult().getIdToken(), null);
- return mAuth.signInWithCredential(authCredential);
- });
- } else {
- return mAuth.signInWithEmailAndPassword(email1, password);
- }
- });
- }
-
/**
* Signs the current user out, if one is signed in.
*
@@ -392,42 +277,19 @@ public Task signOut(@NonNull Context context) {
if (!playServicesAvailable) {
Log.w(TAG, "Google Play services not available during signOut");
}
-
- Task maybeDisableAutoSignIn = playServicesAvailable
- ? GoogleApiUtils.getCredentialsClient(context).disableAutoSignIn()
- : Tasks.forResult((Void) null);
-
- maybeDisableAutoSignIn
- .continueWith(task -> {
- // We want to ignore a specific exception, since it's not a good reason
- // to fail (see Issue 1156).
- Exception e = task.getException();
- if (e instanceof ApiException
- && ((ApiException) e).getStatusCode() == CommonStatusCodes
- .CANCELED) {
- Log.w(TAG, "Could not disable auto-sign in, maybe there are no " +
- "SmartLock accounts available?", e);
- return null;
- }
-
- return task.getResult();
- });
-
- return Tasks.whenAll(
- signOutIdps(context),
- maybeDisableAutoSignIn
- ).continueWith(task -> {
- task.getResult(); // Propagate exceptions
+ signOutIdps(context);
+ Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
+ return clearCredentialState(context, singleThreadExecutor).continueWith(task -> {
+ task.getResult(); // Propagate exceptions if any.
mAuth.signOut();
return null;
});
}
/**
- * Delete the use from FirebaseAuth and delete any associated credentials from the Credentials
- * API. Returns a {@link Task} that succeeds if the Firebase Auth user deletion succeeds and
- * fails if the Firebase Auth deletion fails. Credentials deletion failures are handled
- * silently.
+ * Delete the user from FirebaseAuth.
+ *
+ *
Any associated saved credentials are not explicitly deleted with the new APIs.
*
* @param context the calling {@link Context}.
*/
@@ -439,41 +301,10 @@ public Task delete(@NonNull final Context context) {
String.valueOf(CommonStatusCodes.SIGN_IN_REQUIRED),
"No currently signed in user."));
}
-
- final List credentials = getCredentialsFromFirebaseUser(currentUser);
-
- // Ensure the order in which tasks are executed properly destructures the user.
- return signOutIdps(context).continueWithTask(task -> {
- task.getResult(); // Propagate exception if there was one
-
- if (!GoogleApiUtils.isPlayServicesAvailable(context)) {
- Log.w(TAG, "Google Play services not available during delete");
- return Tasks.forResult((Void) null);
- }
-
- final CredentialsClient client = GoogleApiUtils.getCredentialsClient(context);
- List> credentialTasks = new ArrayList<>();
- for (Credential credential : credentials) {
- credentialTasks.add(client.delete(credential));
- }
- return Tasks.whenAll(credentialTasks)
- .continueWith(task1 -> {
- Exception e = task1.getException();
- Throwable t = e == null ? null : e.getCause();
- if (!(t instanceof ApiException)
- || ((ApiException) t).getStatusCode() !=
- CommonStatusCodes.CANCELED) {
- // Only propagate the exception if it isn't an invalid account
- // one. This can occur if we failed to save the credential or it
- // was deleted elsewhere. However, a lack of stored credential
- // doesn't mean fully deleting the user failed.
- return task1.getResult();
- }
-
- return null;
- });
- }).continueWithTask(task -> {
- task.getResult(); // Propagate exception if there was one
+ signOutIdps(context);
+ Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
+ return clearCredentialState(context, singleThreadExecutor).continueWithTask(task -> {
+ task.getResult(); // Propagate exceptions if any.
return currentUser.delete();
});
}
@@ -506,15 +337,43 @@ public int getEmulatorPort() {
return mEmulatorPort;
}
- private Task signOutIdps(@NonNull Context context) {
+ private void signOutIdps(@NonNull Context context) {
if (ProviderAvailability.IS_FACEBOOK_AVAILABLE) {
LoginManager.getInstance().logOut();
}
- if (GoogleApiUtils.isPlayServicesAvailable(context)) {
- return GoogleSignIn.getClient(context, GoogleSignInOptions.DEFAULT_SIGN_IN).signOut();
- } else {
- return Tasks.forResult((Void) null);
- }
+ }
+
+ /**
+ * A Task to clear the credential state in Credential Manager.
+ * @param context
+ * @param executor
+ * @return
+ */
+ private Task clearCredentialState(
+ @NonNull Context context,
+ @NonNull Executor executor
+ ) {
+ TaskCompletionSource completionSource = new TaskCompletionSource<>();
+
+ ClearCredentialStateRequest clearRequest = new ClearCredentialStateRequest();
+ GoogleApiUtils.getCredentialManager(context)
+ .clearCredentialStateAsync(
+ clearRequest,
+ new CancellationSignal(),
+ executor,
+ new CredentialManagerCallback<>() {
+ @Override
+ public void onResult(Void unused) {
+ completionSource.setResult(unused);
+ }
+
+ @Override
+ public void onError(@NonNull ClearCredentialException e) {
+ completionSource.setException(e);
+ }
+ }
+ );
+ return completionSource.getTask();
}
/**
@@ -1294,7 +1153,6 @@ private abstract class AuthIntentBuilder {
boolean mAlwaysShowProviderChoice = false;
boolean mLockOrientation = false;
boolean mEnableCredentials = true;
- boolean mEnableHints = true;
AuthMethodPickerLayout mAuthMethodPickerLayout = null;
ActionCodeSettings mPasswordSettings = null;
@@ -1419,32 +1277,15 @@ public T setDefaultProvider(@Nullable IdpConfig config) {
}
/**
- * Enables or disables the use of Smart Lock for Passwords in the sign in flow. To
- * (en)disable hint selector and credential selector independently use {@link
- * #setIsSmartLockEnabled(boolean, boolean)}
- *
- *
SmartLock is enabled by default.
- *
- * @param enabled enables smartlock's credential selector and hint selector
- */
- @NonNull
- public T setIsSmartLockEnabled(boolean enabled) {
- return setIsSmartLockEnabled(enabled, enabled);
- }
-
- /**
- * Enables or disables the use of Smart Lock for Passwords credential selector and hint
- * selector.
+ * Enables or disables the use of Credential Manager for Passwords credential selector
*
- *
Both selectors are enabled by default.
+ *
Is enabled by default.
*
* @param enableCredentials enables credential selector before signup
- * @param enableHints enable hint selector in respective signup screens
*/
@NonNull
- public T setIsSmartLockEnabled(boolean enableCredentials, boolean enableHints) {
+ public T setCredentialManagerEnabled(boolean enableCredentials) {
mEnableCredentials = enableCredentials;
- mEnableHints = enableHints;
return (T) this;
}
@@ -1577,7 +1418,6 @@ protected FlowParameters getFlowParams() {
mTosUrl,
mPrivacyPolicyUrl,
mEnableCredentials,
- mEnableHints,
mEnableAnonymousUpgrade,
mAlwaysShowProviderChoice,
mLockOrientation,
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 bdbc63c34..3fa619bef 100644
--- a/auth/src/main/java/com/firebase/ui/auth/KickoffActivity.java
+++ b/auth/src/main/java/com/firebase/ui/auth/KickoffActivity.java
@@ -25,7 +25,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class KickoffActivity extends InvisibleActivityBase {
- private SignInKickstarter mKickstarter;
+ public static SignInKickstarter mKickstarter;
public static Intent createIntent(Context context, FlowParameters flowParams) {
return createBaseIntent(context, KickoffActivity.class, flowParams);
@@ -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/FlowParameters.java b/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.java
index b43edda53..c6dac950a 100644
--- a/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.java
+++ b/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.java
@@ -70,7 +70,6 @@ public FlowParameters createFromParcel(Parcel in) {
termsOfServiceUrl,
privacyPolicyUrl,
enableCredentials,
- enableHints,
enableAnonymousUpgrade,
alwaysShowProviderChoice,
lockOrientation,
@@ -113,7 +112,6 @@ public FlowParameters[] newArray(int size) {
public final ActionCodeSettings passwordResetSettings;
public final boolean enableCredentials;
- public final boolean enableHints;
public final boolean enableAnonymousUpgrade;
public final boolean alwaysShowProviderChoice;
public final boolean lockOrientation;
@@ -130,7 +128,6 @@ public FlowParameters(
@Nullable String termsOfServiceUrl,
@Nullable String privacyPolicyUrl,
boolean enableCredentials,
- boolean enableHints,
boolean enableAnonymousUpgrade,
boolean alwaysShowProviderChoice,
boolean lockOrientation,
@@ -146,7 +143,6 @@ public FlowParameters(
this.termsOfServiceUrl = termsOfServiceUrl;
this.privacyPolicyUrl = privacyPolicyUrl;
this.enableCredentials = enableCredentials;
- this.enableHints = enableHints;
this.enableAnonymousUpgrade = enableAnonymousUpgrade;
this.alwaysShowProviderChoice = alwaysShowProviderChoice;
this.lockOrientation = lockOrientation;
@@ -172,7 +168,6 @@ public void writeToParcel(Parcel dest, int flags) {
dest.writeString(termsOfServiceUrl);
dest.writeString(privacyPolicyUrl);
dest.writeInt(enableCredentials ? 1 : 0);
- dest.writeInt(enableHints ? 1 : 0);
dest.writeInt(enableAnonymousUpgrade ? 1 : 0);
dest.writeInt(alwaysShowProviderChoice ? 1 : 0);
dest.writeInt(lockOrientation ? 1 : 0);
@@ -205,7 +200,6 @@ public boolean isAnonymousUpgradeEnabled() {
public boolean isPlayServicesRequired() {
// Play services only required for Google Sign In and the Credentials API
return isProviderEnabled(GoogleAuthProvider.PROVIDER_ID)
- || enableHints
|| enableCredentials;
}
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 5cc3a38f3..000000000
--- a/auth/src/main/java/com/firebase/ui/auth/data/remote/SignInKickstarter.java
+++ /dev/null
@@ -1,251 +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 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.auth.api.credentials.Credential;
-import com.google.android.gms.auth.api.credentials.CredentialRequest;
-import com.google.android.gms.auth.api.credentials.CredentialRequestResponse;
-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.OnCompleteListener;
-import com.google.android.gms.tasks.OnFailureListener;
-import com.google.android.gms.tasks.OnSuccessListener;
-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 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 {
- 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;
- }
-
- // Signing in with Generic IDP puts the app in the background - it can be reclaimed by the
- // OS during the sign in flow.
- Task pendingResultTask = getAuth().getPendingAuthResult();
- if (pendingResultTask != null) {
- pendingResultTask
- .addOnSuccessListener(
- authResult -> {
- final IdpResponse response = new IdpResponse.Builder(
- new User.Builder(
- authResult.getCredential().getProvider(),
- authResult.getUser().getEmail()).build())
- .build();
- handleSuccess(response, authResult);
-
- })
- .addOnFailureListener(
- e -> setResult(Resource.forFailure(e)));
- return;
- }
-
-
- // Only support password credentials if email auth is enabled
- boolean supportPasswords = ProviderUtils.getConfigFromIdps(
- getArguments().providers, EmailAuthProvider.PROVIDER_ID) != null;
- List accountTypes = getCredentialAccountTypes();
-
- // If the request will be empty, avoid the step entirely
- boolean willRequestCredentials = supportPasswords || accountTypes.size() > 0;
-
- if (getArguments().enableCredentials && willRequestCredentials) {
- setResult(Resource.forLoading());
-
- GoogleApiUtils.getCredentialsClient(getApplication())
- .request(new CredentialRequest.Builder()
- .setPasswordLoginSupported(supportPasswords)
- .setAccountTypes(accountTypes.toArray(new String[accountTypes.size()]))
- .build())
- .addOnCompleteListener(task -> {
- try {
- handleCredential(
- task.getResult(ApiException.class).getCredential());
- } catch (ResolvableApiException e) {
- if (e.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
- setResult(Resource.forFailure(
- new PendingIntentRequiredException(
- e.getResolution(), RequestCodes.CRED_HINT)));
- } else {
- startAuthMethodChoice();
- }
- } catch (ApiException e) {
- startAuthMethodChoice();
- }
- });
- } else {
- 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) {
- handleCredential((Credential) data.getParcelableExtra(Credential.EXTRA_KEY));
- } 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()));
- }
- }
- }
-
- private void handleCredential(final Credential credential) {
- String id = credential.getId();
- String password = credential.getPassword();
- if (TextUtils.isEmpty(password)) {
- String identity = credential.getAccountType();
- if (identity == null) {
- startAuthMethodChoice();
- } else {
- redirectSignIn(
- ProviderUtils.accountTypeToProviderId(credential.getAccountType()), id);
- }
- } else {
- final IdpResponse response = new IdpResponse.Builder(
- new User.Builder(EmailAuthProvider.PROVIDER_ID, id).build()).build();
-
- setResult(Resource.forLoading());
- getAuth().signInWithEmailAndPassword(id, password)
- .addOnSuccessListener(result -> handleSuccess(response, result))
- .addOnFailureListener(e -> {
- if (e instanceof FirebaseAuthInvalidUserException
- || e instanceof FirebaseAuthInvalidCredentialsException) {
- // In this case the credential saved in SmartLock was not
- // a valid credential, we should delete it from SmartLock
- // before continuing.
- GoogleApiUtils.getCredentialsClient(getApplication())
- .delete(credential);
- }
- 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 a21918638..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,11 +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.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;
@@ -45,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);
@@ -72,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.java b/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.java
deleted file mode 100644
index 9807cde63..000000000
--- a/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.firebase.ui.auth.ui.credentials;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-
-import com.firebase.ui.auth.IdpResponse;
-import com.firebase.ui.auth.data.model.FlowParameters;
-import com.firebase.ui.auth.data.model.Resource;
-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.smartlock.SmartLockHandler;
-import com.google.android.gms.auth.api.credentials.Credential;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.ViewModelProvider;
-
-/**
- * Invisible Activity used for saving credentials to SmartLock.
- */
-public class CredentialSaveActivity extends InvisibleActivityBase {
- private static final String TAG = "CredentialSaveActivity";
-
- private SmartLockHandler mHandler;
-
- @NonNull
- public static Intent createIntent(Context context,
- FlowParameters flowParams,
- Credential credential,
- IdpResponse response) {
- return createBaseIntent(context, CredentialSaveActivity.class, flowParams)
- .putExtra(ExtraConstants.CREDENTIAL, credential)
- .putExtra(ExtraConstants.IDP_RESPONSE, response);
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final IdpResponse response = getIntent().getParcelableExtra(ExtraConstants.IDP_RESPONSE);
- Credential credential = getIntent().getParcelableExtra(ExtraConstants.CREDENTIAL);
-
- mHandler = new ViewModelProvider(this).get(SmartLockHandler.class);
- mHandler.init(getFlowParams());
- mHandler.setResponse(response);
-
- mHandler.getOperation().observe(this, new ResourceObserver(this) {
- @Override
- protected void onSuccess(@NonNull IdpResponse response) {
- finish(RESULT_OK, response.toIntent());
- }
-
- @Override
- protected void onFailure(@NonNull Exception e) {
- // RESULT_OK since we don't want to halt sign-in just because of a credential save
- // error.
- finish(RESULT_OK, response.toIntent());
- }
- });
-
- // Avoid double-saving
- Resource currentOp = mHandler.getOperation().getValue();
- if (currentOp == null) {
- Log.d(TAG, "Launching save operation.");
- mHandler.saveCredentials(credential);
- } else {
- Log.d(TAG, "Save operation in progress, doing nothing.");
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- mHandler.onActivityResult(requestCode, resultCode);
- }
-}
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
new file mode 100644
index 000000000..c539de0c7
--- /dev/null
+++ b/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt
@@ -0,0 +1,86 @@
+package com.firebase.ui.auth.ui.credentials
+
+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
+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.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 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.
+ init(flowParams)
+ // Pass the IdP response if present.
+ response?.let { setResponse(it) }
+
+ // Observe the operation's result.
+ operation.observe(
+ this@CredentialSaveActivity,
+ object : ResourceObserver(this@CredentialSaveActivity) {
+ override fun onSuccess(response: IdpResponse) {
+ finish(RESULT_OK, response.toIntent())
+ }
+
+ override fun onFailure(e: Exception) {
+ // Even if saving fails, do not block the sign-in flow.
+ response?.let {
+ finish(RESULT_OK, it.toIntent())
+ } ?: finish(RESULT_OK, null)
+ }
+ }
+ )
+ }
+
+ val currentOp: Resource? = credentialManagerHandler.operation.value
+
+ if (currentOp == null) {
+ Log.d(TAG, "Launching save operation.")
+ // With the new CredentialManager, pass the email and password directly.
+ val firebaseUser = FirebaseAuth.getInstance().currentUser
+ val email = firebaseUser?.email ?: emailExtra
+
+ credentialManagerHandler.saveCredentials(this, firebaseUser, email, password)
+ } else {
+ Log.d(TAG, "Save operation in progress, doing nothing.")
+ }
+ }
+
+ companion object {
+ private const val TAG = "CredentialSaveActivity"
+
+ @JvmStatic
+ fun createIntent(
+ context: Context,
+ flowParams: FlowParameters,
+ email: String,
+ password: String?,
+ response: IdpResponse
+ ): Intent {
+ return createBaseIntent(context, CredentialSaveActivity::class.java, flowParams).apply {
+ putExtra(ExtraConstants.EMAIL, email)
+ putExtra(ExtraConstants.PASSWORD, password)
+ putExtra(ExtraConstants.IDP_RESPONSE, response)
+ }
+ }
+ }
+}
\ No newline at end of file
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..b34ce6ffa 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);
@@ -90,11 +87,18 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
ImeHelper.setImeOnDoneListener(mEmailEditText, this);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && getFlowParams().enableHints) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
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);
+
+ // Hide sign up button for email link authentication
+ if (getEmailProvider().equals(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD)) {
+ mSignUpButton.setVisibility(View.GONE);
+ }
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 +128,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().enableCredentials) {
+ 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 +150,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 +161,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 +216,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 +226,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/CheckEmailHandler.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.java
index 7498d7c39..3a911a417 100644
--- a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.java
+++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.java
@@ -2,7 +2,9 @@
import android.app.Activity;
import android.app.Application;
+import android.app.PendingIntent;
import android.content.Intent;
+import android.util.Log;
import com.firebase.ui.auth.data.model.PendingIntentRequiredException;
import com.firebase.ui.auth.data.model.Resource;
@@ -10,28 +12,55 @@
import com.firebase.ui.auth.util.data.ProviderUtils;
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.tasks.OnCompleteListener;
+import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.tasks.Task;
-import androidx.annotation.NonNull;
+// New Identity API imports:
+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 androidx.annotation.Nullable;
public class CheckEmailHandler extends AuthViewModelBase {
+ private static final String TAG = "CheckEmailHandler";
+
public CheckEmailHandler(Application application) {
super(application);
}
+ /**
+ * Initiates a hint picker flow using the new Identity API.
+ * This replaces the deprecated Credentials API call.
+ */
public void fetchCredential() {
- setResult(Resource.forFailure(new PendingIntentRequiredException(
- Credentials.getClient(getApplication()).getHintPickerIntent(
- new HintRequest.Builder().setEmailAddressIdentifierSupported(true).build()),
- RequestCodes.CRED_HINT
- )));
+ // Build a sign-in request that supports password-based sign in,
+ // which will trigger the hint picker UI for email addresses.
+ SignInClient signInClient = Identity.getSignInClient(getApplication());
+ BeginSignInRequest signInRequest = BeginSignInRequest.builder()
+ .setPasswordRequestOptions(
+ BeginSignInRequest.PasswordRequestOptions.builder()
+ .setSupported(true)
+ .build())
+ .build();
+
+ signInClient.beginSignIn(signInRequest)
+ .addOnSuccessListener(result -> {
+ // The new API returns a PendingIntent to launch the hint picker.
+ PendingIntent pendingIntent = result.getPendingIntent();
+ setResult(Resource.forFailure(
+ new PendingIntentRequiredException(pendingIntent, RequestCodes.CRED_HINT)));
+ })
+ .addOnFailureListener(e -> {
+ Log.e(TAG, "beginSignIn failed", e);
+ setResult(Resource.forFailure(e));
+ });
}
+ /**
+ * Fetches the top provider for the given email.
+ */
public void fetchProvider(final String email) {
setResult(Resource.forLoading());
ProviderUtils.fetchTopProvider(getAuth(), getArguments(), email)
@@ -45,22 +74,35 @@ public void fetchProvider(final String email) {
});
}
+ /**
+ * Handles the result from the hint picker launched via the new Identity API.
+ */
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) { return; }
+ if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) {
+ return;
+ }
setResult(Resource.forLoading());
- final Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
- final String email = credential.getId();
- ProviderUtils.fetchTopProvider(getAuth(), getArguments(), email)
- .addOnCompleteListener(task -> {
- if (task.isSuccessful()) {
- setResult(Resource.forSuccess(new User.Builder(task.getResult(), email)
- .setName(credential.getName())
- .setPhotoUri(credential.getProfilePictureUri())
- .build()));
- } else {
- setResult(Resource.forFailure(task.getException()));
- }
- });
+ SignInClient signInClient = Identity.getSignInClient(getApplication());
+ try {
+ // Retrieve the SignInCredential from the returned intent.
+ SignInCredential credential = signInClient.getSignInCredentialFromIntent(data);
+ final String email = credential.getId();
+
+ ProviderUtils.fetchTopProvider(getAuth(), getArguments(), email)
+ .addOnCompleteListener(task -> {
+ if (task.isSuccessful()) {
+ setResult(Resource.forSuccess(new User.Builder(task.getResult(), email)
+ .setName(credential.getDisplayName())
+ .setPhotoUri(credential.getProfilePictureUri())
+ .build()));
+ } else {
+ setResult(Resource.forFailure(task.getException()));
+ }
+ });
+ } catch (ApiException e) {
+ Log.e(TAG, "getSignInCredentialFromIntent failed", e);
+ setResult(Resource.forFailure(e));
+ }
}
}
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 58a1c7fcf..000000000
--- a/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java
+++ /dev/null
@@ -1,404 +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.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.R;
-import com.firebase.ui.auth.data.model.FlowParameters;
-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.viewmodel.ProviderSignInBase;
-import com.firebase.ui.auth.viewmodel.ResourceObserver;
-import com.firebase.ui.auth.viewmodel.idp.SocialProviderResponseHandler;
-import com.google.android.material.snackbar.Snackbar;
-import com.google.firebase.auth.EmailAuthProvider;
-import com.google.firebase.auth.FacebookAuthProvider;
-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.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();
- }
- }
- });
- }
-
- 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..5cec78510
--- /dev/null
+++ b/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.kt
@@ -0,0 +1,492 @@
+/*
+ * 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.firebase.ui.auth.util.GoogleApiUtils
+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.
+ GoogleApiUtils.getCredentialManager(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..c011f1a3e 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();
@@ -89,12 +85,12 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
mSmsTermsText.setText(getString(R.string.fui_sms_terms_of_service,
getString(R.string.fui_verify_phone_number)));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && getFlowParams().enableHints) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mPhoneEditText.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
}
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)) {
@@ -234,8 +220,9 @@ private void setDefaultCountryForSpinner() {
"",
countryIso,
String.valueOf(PhoneNumberUtils.getCountryCode(countryIso))));
- } else if (getFlowParams().enableHints) {
- mCheckPhoneHandler.fetchCredential();
+ } else if (getFlowParams().enableCredentials) {
+ // 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.
*