Instead of building a SmartLock {@link com.google.android.gms.auth.api.credentials.Credential}, + * we now extract the user's email (or phone number as a fallback) and pass it along with the + * password and response.
+ * + * @param firebaseUser the currently signed-in user. + * @param response the IdP response. + * @param password the password used during sign-in (may be {@code null}). + */ public void startSaveCredentials( FirebaseUser firebaseUser, IdpResponse response, @Nullable String password) { - // Build credential - String accountType = ProviderUtils.idpResponseToAccountType(response); - Credential credential = CredentialUtils.buildCredential( - firebaseUser, password, accountType); - - // Start the dedicated SmartLock Activity + // Extract email; if null, fallback to the phone number. + String email = firebaseUser.getEmail(); + if (email == null) { + email = firebaseUser.getPhoneNumber(); + } + // Start the dedicated CredentialManager Activity. Intent intent = CredentialSaveActivity.createIntent( - this, getFlowParams(), credential, response); + this, getFlowParams(), email, password, response); startActivityForResult(intent, RequestCodes.CRED_SAVE_FLOW); } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt b/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt index 6c2aa6506..c539de0c7 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/credentials/CredentialSaveActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log +import androidx.lifecycle.ViewModelProvider import com.firebase.ui.auth.IdpResponse import com.firebase.ui.auth.data.model.FlowParameters import com.firebase.ui.auth.data.model.Resource @@ -11,42 +12,37 @@ import com.firebase.ui.auth.ui.InvisibleActivityBase import com.firebase.ui.auth.util.ExtraConstants import com.firebase.ui.auth.viewmodel.ResourceObserver import com.firebase.ui.auth.viewmodel.credentialmanager.CredentialManagerHandler -import com.google.android.gms.auth.api.credentials.Credential -import androidx.lifecycle.ViewModelProvider import com.google.firebase.auth.FirebaseAuth class CredentialSaveActivity : InvisibleActivityBase() { - - private lateinit var credentialManagerHandler: CredentialManagerHandler override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val response: IdpResponse? = intent.getParcelableExtra(ExtraConstants.IDP_RESPONSE) - val credential: Credential? = intent.getParcelableExtra(ExtraConstants.CREDENTIAL) + val emailExtra: String? = intent.getStringExtra(ExtraConstants.EMAIL) + val password: String? = intent.getStringExtra(ExtraConstants.PASSWORD) credentialManagerHandler = ViewModelProvider(this) .get(CredentialManagerHandler::class.java) .apply { - // Initialize with flow parameters + // Initialize with flow parameters. init(flowParams) - // If we have an IdpResponse, set it so subsequent operations can report results + // Pass the IdP response if present. response?.let { setResponse(it) } - // Observe the operation resource + // Observe the operation's result. operation.observe( this@CredentialSaveActivity, object : ResourceObserverThis method creates a GetPhoneNumberHintIntentRequest and calls + * Identity.getSignInClient(activity).getPhoneNumberHintIntent(request) to retrieve an + * IntentSender. The IntentSender is then wrapped in a PendingIntentRequiredException so that + * the caller can launch the hint flow. + * + *
Note: Update your PendingIntentRequiredException to accept an IntentSender + * rather than a PendingIntent. + * + * @param activity The activity used to retrieve the Phone Number Hint IntentSender. + */ + public void fetchCredential(final Activity activity) { + GetPhoneNumberHintIntentRequest request = GetPhoneNumberHintIntentRequest.builder().build(); + Identity.getSignInClient(activity) + .getPhoneNumberHintIntent(request) + .addOnSuccessListener(result -> { + try { + // The new API returns an IntentSender. + IntentSender intentSender = result.getIntentSender(); + // Update your exception to accept an IntentSender. + setResult(Resource.forFailure(new PendingIntentRequiredException(intentSender, RequestCodes.CRED_HINT))); + } catch (Exception e) { + Log.e(TAG, "Launching the IntentSender failed", e); + setResult(Resource.forFailure(e)); + } + }) + .addOnFailureListener(e -> { + Log.e(TAG, "Phone Number Hint failed", e); + setResult(Resource.forFailure(e)); + }); } - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) { return; } - - Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY); - String formattedPhone = PhoneNumberUtils.formatUsingCurrentCountry( - credential.getId(), getApplication()); - if (formattedPhone != null) { - setResult(Resource.forSuccess(PhoneNumberUtils.getPhoneNumber(formattedPhone))); + /** + * Handles the result from the Phone Number Hint flow. + * + *
Call this method from your Activity's onActivityResult. It extracts the phone number from the + * returned Intent and formats it. + * + * @param activity The activity used to process the returned Intent. + * @param requestCode The request code (should match RequestCodes.CRED_HINT). + * @param resultCode The result code from the hint flow. + * @param data The Intent data returned from the hint flow. + */ + public void onActivityResult(Activity activity, int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) { + return; + } + try { + String phoneNumber = Identity.getSignInClient(activity).getPhoneNumberFromIntent(data); + String formattedPhone = PhoneNumberUtils.formatUsingCurrentCountry(phoneNumber, getApplication()); + if (formattedPhone != null) { + setResult(Resource.forSuccess(PhoneNumberUtils.getPhoneNumber(formattedPhone))); + } else { + setResult(Resource.forFailure(new Exception("Failed to format phone number"))); + } + } catch (Exception e) { + Log.e(TAG, "Phone Number Hint failed", e); + setResult(Resource.forFailure(e)); } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java index a02e3057e..5dae792ad 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CheckPhoneNumberFragment.java @@ -30,9 +30,6 @@ import androidx.annotation.RestrictTo; import androidx.lifecycle.ViewModelProvider; -/** - * Displays country selector and phone number input form for users - */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class CheckPhoneNumberFragment extends FragmentBase implements View.OnClickListener { public static final String TAG = "VerifyPhoneFragment"; @@ -50,7 +47,6 @@ public class CheckPhoneNumberFragment extends FragmentBase implements View.OnCli private TextView mSmsTermsText; private TextView mFooterText; - public static CheckPhoneNumberFragment newInstance(Bundle params) { CheckPhoneNumberFragment fragment = new CheckPhoneNumberFragment(); Bundle args = new Bundle(); @@ -94,7 +90,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat } requireActivity().setTitle(getString(R.string.fui_verify_phone_number_title)); - ImeHelper.setImeOnDoneListener(mPhoneEditText, () -> onNext()); + ImeHelper.setImeOnDoneListener(mPhoneEditText, this::onNext); mSubmitButton.setOnClickListener(this); setupPrivacyDisclosures(); @@ -112,24 +108,24 @@ protected void onSuccess(@NonNull PhoneNumber number) { @Override protected void onFailure(@NonNull Exception e) { - // Just let the user enter their data + // Let the user enter their data if hint retrieval fails } }); if (savedInstanceState != null || mCalled) { return; } - // Fragment back stacks are the stuff of nightmares (what's new?): the fragment isn't - // destroyed so its state isn't saved and we have to rely on an instance field. Sigh. + // Fragment back stacks can cause state retention so we rely on an instance field. mCalled = true; - // DON'T REMOVE + // Set default country or prompt for phone number using the Phone Number Hint flow. setDefaultCountryForSpinner(); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - mCheckPhoneHandler.onActivityResult(requestCode, resultCode, data); + // Pass the activity instance to the handler + mCheckPhoneHandler.onActivityResult(requireActivity(), requestCode, resultCode, data); } @Override @@ -165,18 +161,15 @@ private void onNext() { @Nullable private String getPseudoValidPhoneNumber() { String everythingElse = mPhoneEditText.getText().toString(); - if (TextUtils.isEmpty(everythingElse)) { return null; } - return PhoneNumberUtils.format( everythingElse, mCountryListSpinner.getSelectedCountryInfo()); } private void setupPrivacyDisclosures() { FlowParameters params = getFlowParams(); - boolean termsAndPrivacyUrlsProvided = params.isTermsOfServiceUrlProvided() && params.isPrivacyPolicyUrlProvided(); @@ -188,7 +181,6 @@ private void setupPrivacyDisclosures() { PrivacyDisclosureUtils.setupTermsOfServiceFooter(requireContext(), params, mFooterText); - String verifyText = getString(R.string.fui_verify_phone_number); mSmsTermsText.setText(getString(R.string.fui_sms_terms_of_service, verifyText)); } @@ -202,15 +194,12 @@ private void setCountryCode(PhoneNumber number) { private void setupCountrySpinner() { Bundle params = getArguments().getBundle(ExtraConstants.PARAMS); mCountryListSpinner.init(params, mCountryListAnchor); - - // Clear error when spinner is clicked on + // Clear error when spinner is clicked mCountryListSpinner.setOnClickListener(v -> mPhoneInputLayout.setError(null)); } private void setDefaultCountryForSpinner() { - // Check for phone - // It is assumed that the phone number that are being wired in via Credential Selector - // are e164 since we store it. + // Check for phone number defaults Bundle params = getArguments().getBundle(ExtraConstants.PARAMS); String phone = null; String countryIso = null; @@ -221,10 +210,7 @@ private void setDefaultCountryForSpinner() { nationalNumber = params.getString(ExtraConstants.NATIONAL_NUMBER); } - // We can receive the phone number in one of two formats: split between the ISO or fully - // processed. If it's complete, we use it directly. Otherwise, we parse the ISO and national - // number combination or we just set the default ISO if there's no default number. If there - // are no defaults at all, we prompt the user for a phone number through Smart Lock. + // If phone is provided in full, use it. Otherwise, parse ISO and national number or prompt for a phone hint. if (!TextUtils.isEmpty(phone)) { start(PhoneNumberUtils.getPhoneNumber(phone)); } else if (!TextUtils.isEmpty(countryIso) && !TextUtils.isEmpty(nationalNumber)) { @@ -235,7 +221,8 @@ private void setDefaultCountryForSpinner() { countryIso, String.valueOf(PhoneNumberUtils.getCountryCode(countryIso)))); } else if (getFlowParams().enableHints) { - mCheckPhoneHandler.fetchCredential(); + // Launch phone number hint flow using the new API + mCheckPhoneHandler.fetchCredential(requireActivity()); } } diff --git a/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java b/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java index df318ad64..ca21a9553 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/CredentialUtils.java @@ -4,8 +4,6 @@ import android.text.TextUtils; import android.util.Log; -import com.firebase.ui.auth.IdpResponse; -import com.google.android.gms.auth.api.credentials.Credential; import com.google.firebase.auth.FirebaseUser; import androidx.annotation.NonNull; @@ -13,10 +11,10 @@ import androidx.annotation.RestrictTo; /** - * Utility class for working with {@link Credential} objects. + * Utility class for extracting credential data from a {@link FirebaseUser} for the new CredentialManager. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class CredentialUtils { +public final class CredentialUtils { private static final String TAG = "CredentialUtils"; @@ -25,54 +23,85 @@ private CredentialUtils() { } /** - * Build a credential for the specified {@link FirebaseUser} with optional password and {@link - * IdpResponse}. + * Extracts the necessary data from the specified {@link FirebaseUser} along with the user's password. *
- * If the credential cannot be built (for example, empty email) then will return {@code null}.
+ * If both the email and phone number are missing or the password is empty, this method returns {@code null}.
+ *
+ * @param user the FirebaseUser from which to extract data.
+ * @param password the password the user signed in with.
+ * @return a {@link CredentialData} instance containing the user’s sign-in information, or {@code null} if insufficient data.
*/
@Nullable
- public static Credential buildCredential(@NonNull FirebaseUser user,
- @Nullable String password,
- @Nullable String accountType) {
+ public static CredentialData buildCredentialData(@NonNull FirebaseUser user,
+ @Nullable String password) {
String email = user.getEmail();
String phone = user.getPhoneNumber();
- Uri profilePictureUri =
- user.getPhotoUrl() == null ? null : Uri.parse(user.getPhotoUrl().toString());
+ Uri profilePictureUri = (user.getPhotoUrl() != null)
+ ? Uri.parse(user.getPhotoUrl().toString())
+ : null;
if (TextUtils.isEmpty(email) && TextUtils.isEmpty(phone)) {
- Log.w(TAG, "User (accountType=" + accountType + ") has no email or phone number, cannot build credential.");
+ Log.w(TAG, "User has no email or phone number; cannot build credential data.");
return null;
}
- if (password == null && accountType == null) {
- Log.w(TAG, "User has no accountType or password, cannot build credential.");
+ if (TextUtils.isEmpty(password)) {
+ Log.w(TAG, "Password is required to build credential data.");
return null;
}
- Credential.Builder builder =
- new Credential.Builder(TextUtils.isEmpty(email) ? phone : email)
- .setName(user.getDisplayName())
- .setProfilePictureUri(profilePictureUri);
+ // Prefer email if available; otherwise fall back to phone.
+ String identifier = !TextUtils.isEmpty(email) ? email : phone;
+ return new CredentialData(identifier, user.getDisplayName(), password, profilePictureUri);
+ }
- if (TextUtils.isEmpty(password)) {
- builder.setAccountType(accountType);
- } else {
- builder.setPassword(password);
+ /**
+ * Same as {@link #buildCredentialData(FirebaseUser, String)} but throws an exception if data cannot be built.
+ *
+ * @param user the FirebaseUser.
+ * @param password the password the user signed in with.
+ * @return a non-null {@link CredentialData} instance.
+ * @throws IllegalStateException if credential data cannot be constructed.
+ */
+ @NonNull
+ public static CredentialData buildCredentialDataOrThrow(@NonNull FirebaseUser user,
+ @Nullable String password) {
+ CredentialData credentialData = buildCredentialData(user, password);
+ if (credentialData == null) {
+ throw new IllegalStateException("Unable to build credential data");
}
-
- return builder.build();
+ return credentialData;
}
/**
- * @see #buildCredential(FirebaseUser, String, String)
+ * A simple data class representing the information required by the new CredentialManager.
*/
- @NonNull
- public static Credential buildCredentialOrThrow(@NonNull FirebaseUser user,
- @Nullable String password,
- @Nullable String accountType) {
- Credential credential = buildCredential(user, password, accountType);
- if (credential == null) {
- throw new IllegalStateException("Unable to build credential");
+ public static final class CredentialData {
+ private final String identifier;
+ private final String displayName;
+ private final String password;
+ private final Uri profilePictureUri;
+
+ public CredentialData(String identifier, String displayName, String password, Uri profilePictureUri) {
+ this.identifier = identifier;
+ this.displayName = displayName;
+ this.password = password;
+ this.profilePictureUri = profilePictureUri;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public Uri getProfilePictureUri() {
+ return profilePictureUri;
}
- return credential;
}
}
diff --git a/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java b/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java
index 4dd780128..07211a4ea 100644
--- a/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java
+++ b/auth/src/main/java/com/firebase/ui/auth/util/ExtraConstants.java
@@ -27,6 +27,7 @@ public final class ExtraConstants {
public static final String CREDENTIAL = "extra_credential";
public static final String EMAIL = "extra_email";
+ public static final String PASSWORD = "extra_password";
public static final String DEFAULT_EMAIL = "extra_default_email";
public static final String ALLOW_NEW_EMAILS = "extra_allow_new_emails";
public static final String REQUIRE_NAME = "extra_require_name";
diff --git a/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java b/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java
index e1d3d8681..65fe147ef 100644
--- a/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java
+++ b/auth/src/main/java/com/firebase/ui/auth/util/data/ProviderUtils.java
@@ -22,7 +22,6 @@
import com.firebase.ui.auth.IdpResponse;
import com.firebase.ui.auth.R;
import com.firebase.ui.auth.data.model.FlowParameters;
-import com.google.android.gms.auth.api.credentials.IdentityProviders;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
@@ -47,6 +46,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class ProviderUtils {
+ private static final String GOOGLE_ACCOUNT_TYPE = "https://accounts.google.com";
+ private static final String FACEBOOK_ACCOUNT_TYPE = "https://www.facebook.com";
+ private static final String TWITTER_ACCOUNT_TYPE = "https://twitter.com";
private static final String GITHUB_IDENTITY = "https://github.com";
private static final String PHONE_IDENTITY = "https://phone.firebase";
@@ -74,7 +76,6 @@ public static String idpResponseToAccountType(@Nullable IdpResponse response) {
if (response == null) {
return null;
}
-
return providerIdToAccountType(response.getProviderType());
}
@@ -103,22 +104,22 @@ public static String signInMethodToProviderId(@NonNull String method) {
/**
* Translate a Firebase Auth provider ID (such as {@link GoogleAuthProvider#PROVIDER_ID}) to a
- * Credentials API account type (such as {@link IdentityProviders#GOOGLE}).
+ * Credentials API account type.
*/
public static String providerIdToAccountType(
@AuthUI.SupportedProvider @NonNull String providerId) {
switch (providerId) {
case GoogleAuthProvider.PROVIDER_ID:
- return IdentityProviders.GOOGLE;
+ return GOOGLE_ACCOUNT_TYPE;
case FacebookAuthProvider.PROVIDER_ID:
- return IdentityProviders.FACEBOOK;
+ return FACEBOOK_ACCOUNT_TYPE;
case TwitterAuthProvider.PROVIDER_ID:
- return IdentityProviders.TWITTER;
+ return TWITTER_ACCOUNT_TYPE;
case GithubAuthProvider.PROVIDER_ID:
return GITHUB_IDENTITY;
case PhoneAuthProvider.PROVIDER_ID:
return PHONE_IDENTITY;
- // The account type for email/password creds is null
+ // The account type for email/password creds is null.
case EmailAuthProvider.PROVIDER_ID:
default:
return null;
@@ -128,11 +129,11 @@ public static String providerIdToAccountType(
@AuthUI.SupportedProvider
public static String accountTypeToProviderId(@NonNull String accountType) {
switch (accountType) {
- case IdentityProviders.GOOGLE:
+ case GOOGLE_ACCOUNT_TYPE:
return GoogleAuthProvider.PROVIDER_ID;
- case IdentityProviders.FACEBOOK:
+ case FACEBOOK_ACCOUNT_TYPE:
return FacebookAuthProvider.PROVIDER_ID;
- case IdentityProviders.TWITTER:
+ case TWITTER_ACCOUNT_TYPE:
return TwitterAuthProvider.PROVIDER_ID;
case GITHUB_IDENTITY:
return GithubAuthProvider.PROVIDER_ID;
@@ -215,7 +216,7 @@ public Task> then(@NonNull Task
> then(@NonNull Task
> then(@NonNull Task