diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthMethodPickerLayout.java b/auth/src/main/java/com/firebase/ui/auth/AuthMethodPickerLayout.java index 3644f2bcc..0e10eb1c4 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthMethodPickerLayout.java +++ b/auth/src/main/java/com/firebase/ui/auth/AuthMethodPickerLayout.java @@ -198,9 +198,11 @@ public AuthMethodPickerLayout build() { } for (String key : providersMapping.keySet()) { - if (!AuthUI.SUPPORTED_PROVIDERS.contains(key) - && !AuthUI.SUPPORTED_OAUTH_PROVIDERS.contains(key)) { - throw new IllegalArgumentException("Unknown provider: " + key); + if (key == null) continue; + if (!AuthUI.isSupportedProvider(key) + && !AuthUI.isSupportedOAuthProvider(key)) { + throw new IllegalStateException( + "Unknown provider: " + key); } } diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java deleted file mode 100644 index d389af20b..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java +++ /dev/null @@ -1,1402 +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; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -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.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.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; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import androidx.annotation.CallSuper; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.StringDef; -import androidx.annotation.StyleRes; - -/** - * The entry point to the AuthUI authentication flow, and related utility methods. If your - * application uses the default {@link FirebaseApp} instance, an AuthUI instance can be retrieved - * simply by calling {@link AuthUI#getInstance()}. If an alternative app instance is in use, call - * {@link AuthUI#getInstance(FirebaseApp)} instead, passing the appropriate app instance. - *

- *

- * See the - * README - * for examples on how to get started with FirebaseUI Auth. - */ -public final class AuthUI { - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static final String TAG = "AuthUI"; - - /** - * Provider for anonymous users. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static final String ANONYMOUS_PROVIDER = "anonymous"; - public static final String EMAIL_LINK_PROVIDER = EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD; - - public static final String MICROSOFT_PROVIDER = "microsoft.com"; - public static final String YAHOO_PROVIDER = "yahoo.com"; - public static final String APPLE_PROVIDER = "apple.com"; - - /** - * Default value for logo resource, omits the logo from the {@link AuthMethodPickerActivity}. - */ - public static final int NO_LOGO = -1; - - /** - * The set of authentication providers supported in Firebase Auth UI. - */ - public static final Set SUPPORTED_PROVIDERS = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - GoogleAuthProvider.PROVIDER_ID, - FacebookAuthProvider.PROVIDER_ID, - TwitterAuthProvider.PROVIDER_ID, - GithubAuthProvider.PROVIDER_ID, - EmailAuthProvider.PROVIDER_ID, - PhoneAuthProvider.PROVIDER_ID, - ANONYMOUS_PROVIDER, - EMAIL_LINK_PROVIDER - ))); - - /** - * The set of OAuth2.0 providers supported in Firebase Auth UI through Generic IDP (web flow). - */ - public static final Set SUPPORTED_OAUTH_PROVIDERS = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - MICROSOFT_PROVIDER, - YAHOO_PROVIDER, - APPLE_PROVIDER, - TwitterAuthProvider.PROVIDER_ID, - GithubAuthProvider.PROVIDER_ID - ))); - - /** - * The set of social authentication providers supported in Firebase Auth UI using their SDK. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static final Set SOCIAL_PROVIDERS = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - GoogleAuthProvider.PROVIDER_ID, - FacebookAuthProvider.PROVIDER_ID))); - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static final String UNCONFIGURED_CONFIG_VALUE = "CHANGE-ME"; - - private static final IdentityHashMap INSTANCES = new IdentityHashMap<>(); - - private static Context sApplicationContext; - - private final FirebaseApp mApp; - private final FirebaseAuth mAuth; - - private String mEmulatorHost = null; - private int mEmulatorPort = -1; - - private AuthUI(FirebaseApp app) { - mApp = app; - mAuth = FirebaseAuth.getInstance(mApp); - - try { - mAuth.setFirebaseUIVersion(BuildConfig.VERSION_NAME); - } catch (Exception e) { - Log.e(TAG, "Couldn't set the FUI version.", e); - } - mAuth.useAppLanguage(); - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @NonNull - public static Context getApplicationContext() { - return sApplicationContext; - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static void setApplicationContext(@NonNull Context context) { - sApplicationContext = Preconditions.checkNotNull(context, "App context cannot be null.") - .getApplicationContext(); - } - - /** - * Retrieves the {@link AuthUI} instance associated with the default app, as returned by {@code - * FirebaseApp.getInstance()}. - * - * @throws IllegalStateException if the default app is not initialized. - */ - @NonNull - public static AuthUI getInstance() { - return getInstance(FirebaseApp.getInstance()); - } - - /** - * Retrieves the {@link AuthUI} instance associated the the specified app name. - * - * @throws IllegalStateException if the app is not initialized. - */ - @NonNull - public static AuthUI getInstance(@NonNull String appName) { - return getInstance(FirebaseApp.getInstance(appName)); - } - - /** - * Retrieves the {@link AuthUI} instance associated the the specified app. - */ - @NonNull - public static AuthUI getInstance(@NonNull FirebaseApp app) { - String releaseUrl = "https://github.com/firebase/FirebaseUI-Android/releases/tag/6.2.0"; - String devWarning = "Beginning with FirebaseUI 6.2.0 you no longer need to include %s to " + - "sign in with %s. Go to %s for more information"; - if (ProviderAvailability.IS_TWITTER_AVAILABLE) { - Log.w(TAG, String.format(devWarning, "the TwitterKit SDK", "Twitter", releaseUrl)); - } - if (ProviderAvailability.IS_GITHUB_AVAILABLE) { - Log.w(TAG, String.format(devWarning, "com.firebaseui:firebase-ui-auth-github", - "GitHub", releaseUrl)); - } - - AuthUI authUi; - synchronized (INSTANCES) { - authUi = INSTANCES.get(app); - if (authUi == null) { - authUi = new AuthUI(app); - INSTANCES.put(app, authUi); - } - } - return authUi; - } - - @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public FirebaseApp getApp() { - return mApp; - } - - @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public FirebaseAuth getAuth() { - return mAuth; - } - - /** - * Returns true if AuthUI can handle the intent. - *

- * AuthUI handle the intent when the embedded data is an email link. If it is, you can then - * specify the link in {@link SignInIntentBuilder#setEmailLink(String)} before starting AuthUI - * and it will be handled immediately. - */ - public static boolean canHandleIntent(@NonNull Intent intent) { - if (intent == null || intent.getData() == null) { - return false; - } - String link = intent.getData().toString(); - return FirebaseAuth.getInstance().isSignInWithEmailLink(link); - } - - /** - * Default theme used by {@link SignInIntentBuilder#setTheme(int)} if no theme customization is - * required. - */ - @StyleRes - public static int getDefaultTheme() { - return R.style.FirebaseUI_DefaultMaterialTheme; - } - - /** - * Signs the current user out, if one is signed in. - * - * @param context the context requesting the user be signed out - * @return A task which, upon completion, signals that the user has been signed out ({@link - * Task#isSuccessful()}, or that the sign-out attempt failed unexpectedly !{@link - * Task#isSuccessful()}). - */ - @NonNull - public Task signOut(@NonNull Context context) { - boolean playServicesAvailable = GoogleApiUtils.isPlayServicesAvailable(context); - if (!playServicesAvailable) { - Log.w(TAG, "Google Play services not available during signOut"); - } - - return signOutIdps(context).continueWith(task -> { - task.getResult(); // Propagate exceptions if any. - mAuth.signOut(); - return null; - }); - } - - /** - * Delete the user from FirebaseAuth. - * - *

Any associated saved credentials are not explicitly deleted with the new APIs. - * - * @param context the calling {@link Context}. - */ - @NonNull - public Task delete(@NonNull final Context context) { - final FirebaseUser currentUser = mAuth.getCurrentUser(); - if (currentUser == null) { - return Tasks.forException(new FirebaseAuthInvalidUserException( - String.valueOf(CommonStatusCodes.SIGN_IN_REQUIRED), - "No currently signed in user.")); - } - - return signOutIdps(context).continueWithTask(task -> { - task.getResult(); // Propagate exception if there was one. - return currentUser.delete(); - }); - } - - /** - * Connect to the Firebase Authentication emulator. - * @see FirebaseAuth#useEmulator(String, int) - */ - public void useEmulator(@NonNull String host, int port) { - Preconditions.checkArgument(port >= 0, "Port must be >= 0"); - Preconditions.checkArgument(port <= 65535, "Port must be <= 65535"); - mEmulatorHost = host; - mEmulatorPort = port; - - mAuth.useEmulator(host, port); - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public boolean isUseEmulator() { - return mEmulatorHost != null && mEmulatorPort >= 0; - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public String getEmulatorHost() { - return mEmulatorHost; - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public int getEmulatorPort() { - return mEmulatorPort; - } - - private Task 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); - } - } - - /** - * Starts the process of creating a sign in intent, with the mandatory application context - * parameter. - */ - @NonNull - public SignInIntentBuilder createSignInIntentBuilder() { - return new SignInIntentBuilder(); - } - - @StringDef({ - GoogleAuthProvider.PROVIDER_ID, - FacebookAuthProvider.PROVIDER_ID, - TwitterAuthProvider.PROVIDER_ID, - GithubAuthProvider.PROVIDER_ID, - EmailAuthProvider.PROVIDER_ID, - PhoneAuthProvider.PROVIDER_ID, - ANONYMOUS_PROVIDER, - EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD - }) - @Retention(RetentionPolicy.SOURCE) - public @interface SupportedProvider { - } - - /** - * Configuration for an identity provider. - */ - public static final class IdpConfig implements Parcelable { - public static final Creator CREATOR = new Creator() { - @Override - public IdpConfig createFromParcel(Parcel in) { - return new IdpConfig(in); - } - - @Override - public IdpConfig[] newArray(int size) { - return new IdpConfig[size]; - } - }; - - private final String mProviderId; - private final Bundle mParams; - - private IdpConfig( - @SupportedProvider @NonNull String providerId, - @NonNull Bundle params) { - mProviderId = providerId; - mParams = new Bundle(params); - } - - private IdpConfig(Parcel in) { - mProviderId = in.readString(); - mParams = in.readBundle(getClass().getClassLoader()); - } - - @NonNull - @SupportedProvider - public String getProviderId() { - return mProviderId; - } - - /** - * @return provider-specific options - */ - @NonNull - public Bundle getParams() { - return new Bundle(mParams); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int i) { - parcel.writeString(mProviderId); - parcel.writeBundle(mParams); - } - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - IdpConfig config = (IdpConfig) o; - - return mProviderId.equals(config.mProviderId); - } - - @Override - public final int hashCode() { - return mProviderId.hashCode(); - } - - @Override - public String toString() { - return "IdpConfig{" + - "mProviderId='" + mProviderId + '\'' + - ", mParams=" + mParams + - '}'; - } - - /** - * Base builder for all authentication providers. - * - * @see SignInIntentBuilder#setAvailableProviders(List) - */ - public static class Builder { - private final Bundle mParams = new Bundle(); - @SupportedProvider - private String mProviderId; - - protected Builder(@SupportedProvider @NonNull String providerId) { - if (!SUPPORTED_PROVIDERS.contains(providerId) - && !SUPPORTED_OAUTH_PROVIDERS.contains(providerId)) { - throw new IllegalArgumentException("Unknown provider: " + providerId); - } - mProviderId = providerId; - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @NonNull - protected final Bundle getParams() { - return mParams; - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - protected void setProviderId(@NonNull String providerId) { - mProviderId = providerId; - } - - @CallSuper - @NonNull - public IdpConfig build() { - return new IdpConfig(mProviderId, mParams); - } - } - - /** - * {@link IdpConfig} builder for the email provider. - */ - public static final class EmailBuilder extends Builder { - public EmailBuilder() { - super(EmailAuthProvider.PROVIDER_ID); - } - - /** - * Enables or disables creating new accounts in the email sign in flows. - *

- * Account creation is enabled by default. - */ - @NonNull - public EmailBuilder setAllowNewAccounts(boolean allow) { - getParams().putBoolean(ExtraConstants.ALLOW_NEW_EMAILS, allow); - return this; - } - - /** - * Configures the requirement for the user to enter first and last name in the email - * sign up flow. - *

- * Name is required by default. - */ - @NonNull - public EmailBuilder setRequireName(boolean requireName) { - getParams().putBoolean(ExtraConstants.REQUIRE_NAME, requireName); - return this; - } - - /** - * Enables email link sign in instead of password based sign in. Once enabled, you must - * pass a valid {@link ActionCodeSettings} object using - * {@link #setActionCodeSettings(ActionCodeSettings)} - *

- * You must enable Firebase Dynamic Links in the Firebase Console to use email link - * sign in. - * - * @throws IllegalStateException if {@link ActionCodeSettings} is null or not - * provided with email link enabled. - */ - @NonNull - public EmailBuilder enableEmailLinkSignIn() { - setProviderId(EMAIL_LINK_PROVIDER); - return this; - } - - /** - * Sets the {@link ActionCodeSettings} object to be used for email link sign in. - *

- * {@link ActionCodeSettings#canHandleCodeInApp()} must be set to true, and a valid - * continueUrl must be passed via {@link ActionCodeSettings.Builder#setUrl(String)}. - * This URL must be allowlisted in the Firebase Console. - * - * @throws IllegalStateException if canHandleCodeInApp is set to false - * @throws NullPointerException if ActionCodeSettings is null - */ - @NonNull - public EmailBuilder setActionCodeSettings(ActionCodeSettings actionCodeSettings) { - getParams().putParcelable(ExtraConstants.ACTION_CODE_SETTINGS, actionCodeSettings); - return this; - } - - /** - * Disables allowing email link sign in to occur across different devices. - *

- * This cannot be disabled with anonymous upgrade. - */ - @NonNull - public EmailBuilder setForceSameDevice() { - getParams().putBoolean(ExtraConstants.FORCE_SAME_DEVICE, true); - return this; - } - - /** - * Sets a default sign in email, if the given email has been registered before, then - * it will ask the user for password, if the given email it's not registered, then - * it starts signing up the default email. - */ - @NonNull - public EmailBuilder setDefaultEmail(String email) { - getParams().putString(ExtraConstants.DEFAULT_EMAIL, email); - return this; - } - - @Override - public IdpConfig build() { - if (super.mProviderId.equals(EMAIL_LINK_PROVIDER)) { - ActionCodeSettings actionCodeSettings = - getParams().getParcelable(ExtraConstants.ACTION_CODE_SETTINGS); - Preconditions.checkNotNull(actionCodeSettings, "ActionCodeSettings cannot be " + - "null when using email link sign in."); - if (!actionCodeSettings.canHandleCodeInApp()) { - // Pre-emptively fail if actionCodeSettings are misconfigured. This would - // have happened when calling sendSignInLinkToEmail - throw new IllegalStateException( - "You must set canHandleCodeInApp in your ActionCodeSettings to " + - "true for Email-Link Sign-in."); - } - } - return super.build(); - } - } - - /** - * {@link IdpConfig} builder for the phone provider. - */ - public static final class PhoneBuilder extends Builder { - public PhoneBuilder() { - super(PhoneAuthProvider.PROVIDER_ID); - } - - /** - * @param number the phone number in international format - * @see #setDefaultNumber(String, String) - */ - @NonNull - public PhoneBuilder setDefaultNumber(@NonNull String number) { - Preconditions.checkUnset(getParams(), - "Cannot overwrite previously set phone number", - ExtraConstants.PHONE, - ExtraConstants.COUNTRY_ISO, - ExtraConstants.NATIONAL_NUMBER); - if (!PhoneNumberUtils.isValid(number)) { - throw new IllegalStateException("Invalid phone number: " + number); - } - - getParams().putString(ExtraConstants.PHONE, number); - - return this; - } - - /** - * Set the default phone number that will be used to populate the phone verification - * sign-in flow. - * - * @param iso the phone number's country code - * @param number the phone number in local format - */ - @NonNull - public PhoneBuilder setDefaultNumber(@NonNull String iso, @NonNull String number) { - Preconditions.checkUnset(getParams(), - "Cannot overwrite previously set phone number", - ExtraConstants.PHONE, - ExtraConstants.COUNTRY_ISO, - ExtraConstants.NATIONAL_NUMBER); - if (!PhoneNumberUtils.isValidIso(iso)) { - throw new IllegalStateException("Invalid country iso: " + iso); - } - - getParams().putString(ExtraConstants.COUNTRY_ISO, iso); - getParams().putString(ExtraConstants.NATIONAL_NUMBER, number); - - return this; - } - - /** - * Set the default country code that will be used in the phone verification sign-in - * flow. - * - * @param iso country iso - */ - @NonNull - public PhoneBuilder setDefaultCountryIso(@NonNull String iso) { - Preconditions.checkUnset(getParams(), - "Cannot overwrite previously set phone number", - ExtraConstants.PHONE, - ExtraConstants.COUNTRY_ISO, - ExtraConstants.NATIONAL_NUMBER); - if (!PhoneNumberUtils.isValidIso(iso)) { - throw new IllegalStateException("Invalid country iso: " + iso); - } - - getParams().putString(ExtraConstants.COUNTRY_ISO, - iso.toUpperCase(Locale.getDefault())); - - return this; - } - - - /** - * Sets the country codes available in the country code selector for phone - * authentication. Takes as input a List of both country isos and codes. - * This is not to be called with - * {@link #setBlockedCountries(List)}. - * If both are called, an exception will be thrown. - *

- * Inputting an e-164 country code (e.g. '+1') will include all countries with - * +1 as its code. - * Example input: {'+52', 'us'} - * For a list of country iso or codes, see Alpha-2 isos here: - * https://en.wikipedia.org/wiki/ISO_3166-1 - * and e-164 codes here: https://en.wikipedia.org/wiki/List_of_country_calling_codes - * - * @param countries a non empty case insensitive list of country codes - * and/or isos to be allowlisted - * @throws IllegalArgumentException if an empty allowlist is provided. - * @throws NullPointerException if a null allowlist is provided. - */ - public PhoneBuilder setAllowedCountries( - @NonNull List countries) { - if (getParams().containsKey(ExtraConstants.BLOCKLISTED_COUNTRIES)) { - throw new IllegalStateException( - "You can either allowlist or blocklist country codes for phone " + - "authentication."); - } - - String message = "Invalid argument: Only non-%s allowlists are valid. " + - "To specify no allowlist, do not call this method."; - Preconditions.checkNotNull(countries, String.format(message, "null")); - Preconditions.checkArgument(!countries.isEmpty(), String.format - (message, "empty")); - - addCountriesToBundle(countries, ExtraConstants.ALLOWLISTED_COUNTRIES); - return this; - } - - /** - * Sets the countries to be removed from the country code selector for phone - * authentication. Takes as input a List of both country isos and codes. - * This is not to be called with - * {@link #setAllowedCountries(List)}. - * If both are called, an exception will be thrown. - *

- * Inputting an e-164 country code (e.g. '+1') will include all countries with - * +1 as its code. - * Example input: {'+52', 'us'} - * For a list of country iso or codes, see Alpha-2 codes here: - * https://en.wikipedia.org/wiki/ISO_3166-1 - * and e-164 codes here: https://en.wikipedia.org/wiki/List_of_country_calling_codes - * - * @param countries a non empty case insensitive list of country codes - * and/or isos to be blocklisted - * @throws IllegalArgumentException if an empty blocklist is provided. - * @throws NullPointerException if a null blocklist is provided. - */ - public PhoneBuilder setBlockedCountries( - @NonNull List countries) { - if (getParams().containsKey(ExtraConstants.ALLOWLISTED_COUNTRIES)) { - throw new IllegalStateException( - "You can either allowlist or blocklist country codes for phone " + - "authentication."); - } - - String message = "Invalid argument: Only non-%s blocklists are valid. " + - "To specify no blocklist, do not call this method."; - Preconditions.checkNotNull(countries, String.format(message, "null")); - Preconditions.checkArgument(!countries.isEmpty(), String.format - (message, "empty")); - - addCountriesToBundle(countries, ExtraConstants.BLOCKLISTED_COUNTRIES); - return this; - } - - @Override - public IdpConfig build() { - validateInputs(); - return super.build(); - } - - private void addCountriesToBundle(List CountryIsos, String CountryIsoType) { - ArrayList uppercaseCodes = new ArrayList<>(); - for (String code : CountryIsos) { - uppercaseCodes.add(code.toUpperCase(Locale.getDefault())); - } - - getParams().putStringArrayList(CountryIsoType, uppercaseCodes); - } - - private void validateInputs() { - List allowedCountries = getParams().getStringArrayList( - ExtraConstants.ALLOWLISTED_COUNTRIES); - List blockedCountries = getParams().getStringArrayList( - ExtraConstants.BLOCKLISTED_COUNTRIES); - - if (allowedCountries != null && blockedCountries != null) { - throw new IllegalStateException( - "You can either allowlist or blocked country codes for phone " + - "authentication."); - } else if (allowedCountries != null) { - validateInputs(allowedCountries, true); - - } else if (blockedCountries != null) { - validateInputs(blockedCountries, false); - } - } - - private void validateInputs(List countries, boolean allowed) { - validateCountryInput(countries); - validateDefaultCountryInput(countries, allowed); - } - - private void validateCountryInput(List codes) { - for (String code : codes) { - if (!PhoneNumberUtils.isValidIso(code) && !PhoneNumberUtils.isValid(code)) { - throw new IllegalArgumentException("Invalid input: You must provide a " + - "valid country iso (alpha-2) or code (e-164). e.g. 'us' or '+1'."); - } - } - } - - private void validateDefaultCountryInput(List codes, boolean allowed) { - // A default iso/code can be set via #setDefaultCountryIso() or #setDefaultNumber() - if (getParams().containsKey(ExtraConstants.COUNTRY_ISO) || - getParams().containsKey(ExtraConstants.PHONE)) { - - if (!validateDefaultCountryIso(codes, allowed) - || !validateDefaultPhoneIsos(codes, allowed)) { - throw new IllegalArgumentException("Invalid default country iso. Make " + - "sure it is either part of the allowed list or that you " - + "haven't blocked it."); - } - } - - } - - private boolean validateDefaultCountryIso(List codes, boolean allowed) { - String defaultIso = getDefaultIso(); - return isValidDefaultIso(codes, defaultIso, allowed); - } - - private boolean validateDefaultPhoneIsos(List codes, boolean allowed) { - List phoneIsos = getPhoneIsosFromCode(); - for (String iso : phoneIsos) { - if (isValidDefaultIso(codes, iso, allowed)) { - return true; - } - } - return phoneIsos.isEmpty(); - } - - private boolean isValidDefaultIso(List codes, String iso, boolean allowed) { - if (iso == null) return true; - boolean containsIso = containsCountryIso(codes, iso); - return containsIso && allowed || !containsIso && !allowed; - - } - - private boolean containsCountryIso(List codes, String iso) { - iso = iso.toUpperCase(Locale.getDefault()); - for (String code : codes) { - if (PhoneNumberUtils.isValidIso(code)) { - if (code.equals(iso)) { - return true; - } - } else { - List isos = PhoneNumberUtils.getCountryIsosFromCountryCode(code); - if (isos.contains(iso)) { - return true; - } - } - } - return false; - } - - private List getPhoneIsosFromCode() { - List isos = new ArrayList<>(); - String phone = getParams().getString(ExtraConstants.PHONE); - if (phone != null && phone.startsWith("+")) { - String countryCode = "+" + PhoneNumberUtils.getPhoneNumber(phone) - .getCountryCode(); - List isosToAdd = PhoneNumberUtils. - getCountryIsosFromCountryCode(countryCode); - if (isosToAdd != null) { - isos.addAll(isosToAdd); - } - } - return isos; - } - - private String getDefaultIso() { - return getParams().containsKey(ExtraConstants.COUNTRY_ISO) ? - getParams().getString(ExtraConstants.COUNTRY_ISO) : null; - } - } - - /** - * {@link IdpConfig} builder for the Google provider. - */ - public static final class GoogleBuilder extends Builder { - public GoogleBuilder() { - super(GoogleAuthProvider.PROVIDER_ID); - } - - private void validateWebClientId() { - Preconditions.checkConfigured(getApplicationContext(), - "Check your google-services plugin configuration, the" + - " default_web_client_id string wasn't populated.", - R.string.default_web_client_id); - } - - /** - * Set the scopes that your app will request when using Google sign-in. See all available - * scopes. - * - * @param scopes additional scopes to be requested - */ - @NonNull - public GoogleBuilder setScopes(@NonNull List scopes) { - GoogleSignInOptions.Builder builder = - new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestEmail(); - for (String scope : scopes) { - builder.requestScopes(new Scope(scope)); - } - return setSignInOptions(builder.build()); - } - - /** - * Set the {@link GoogleSignInOptions} to be used for Google sign-in. Standard - * options like requesting the user's email will automatically be added. - * - * @param options sign-in options - */ - @NonNull - public GoogleBuilder setSignInOptions(@NonNull GoogleSignInOptions options) { - Preconditions.checkUnset(getParams(), - "Cannot overwrite previously set sign-in options.", - ExtraConstants.GOOGLE_SIGN_IN_OPTIONS); - - GoogleSignInOptions.Builder builder = new GoogleSignInOptions.Builder(options); - - String clientId = options.getServerClientId(); - if (clientId == null) { - validateWebClientId(); - clientId = getApplicationContext().getString(R.string.default_web_client_id); - } - - // Warn the user that they are _probably_ doing the wrong thing if they - // have not called requestEmail (see issue #1899 and #1621) - boolean hasEmailScope = false; - for (Scope s : options.getScopes()) { - if ("email".equals(s.getScopeUri())) { - hasEmailScope = true; - break; - } - } - if (!hasEmailScope) { - Log.w(TAG, "The GoogleSignInOptions passed to setSignInOptions does not " + - "request the 'email' scope. In most cases this is a mistake! " + - "Call requestEmail() on the GoogleSignInOptions object."); - } - - builder.requestIdToken(clientId); - getParams().putParcelable( - ExtraConstants.GOOGLE_SIGN_IN_OPTIONS, builder.build()); - - return this; - } - - @NonNull - @Override - public IdpConfig build() { - if (!getParams().containsKey(ExtraConstants.GOOGLE_SIGN_IN_OPTIONS)) { - validateWebClientId(); - setScopes(Collections.emptyList()); - } - - return super.build(); - } - } - - /** - * {@link IdpConfig} builder for the Facebook provider. - */ - public static final class FacebookBuilder extends Builder { - private static final String TAG = "FacebookBuilder"; - - public FacebookBuilder() { - super(FacebookAuthProvider.PROVIDER_ID); - if (!ProviderAvailability.IS_FACEBOOK_AVAILABLE) { - throw new RuntimeException( - "Facebook provider cannot be configured " + - "without dependency. Did you forget to add " + - "'com.facebook.android:facebook-login:VERSION' dependency?"); - } - Preconditions.checkConfigured(getApplicationContext(), - "Facebook provider unconfigured. Make sure to add a" + - " `facebook_application_id` string. See the docs for more info:" + - " https://github" + - ".com/firebase/FirebaseUI-Android/blob/master/auth/README" + - ".md#facebook", - R.string.facebook_application_id); - if (getApplicationContext().getString(R.string.facebook_login_protocol_scheme) - .equals("fbYOUR_APP_ID")) { - Log.w(TAG, "Facebook provider unconfigured for Chrome Custom Tabs."); - } - } - - /** - * Specifies the additional permissions that the application will request in the - * Facebook Login SDK. Available permissions can be found here. - */ - @NonNull - public FacebookBuilder setPermissions(@NonNull List permissions) { - getParams().putStringArrayList( - ExtraConstants.FACEBOOK_PERMISSIONS, new ArrayList<>(permissions)); - return this; - } - } - - /** - * {@link IdpConfig} builder for the Anonymous provider. - */ - public static final class AnonymousBuilder extends Builder { - public AnonymousBuilder() { - super(ANONYMOUS_PROVIDER); - } - } - - /** - * {@link IdpConfig} builder for the Twitter provider. - */ - public static final class TwitterBuilder extends GenericOAuthProviderBuilder { - private static final String PROVIDER_NAME = "Twitter"; - - public TwitterBuilder() { - super(TwitterAuthProvider.PROVIDER_ID, PROVIDER_NAME, - R.layout.fui_idp_button_twitter); - } - } - - /** - * {@link IdpConfig} builder for the GitHub provider. - */ - public static final class GitHubBuilder extends GenericOAuthProviderBuilder { - private static final String PROVIDER_NAME = "Github"; - - public GitHubBuilder() { - super(GithubAuthProvider.PROVIDER_ID, PROVIDER_NAME, - R.layout.fui_idp_button_github); - } - - /** - * Specifies the additional permissions to be requested. - * - *

Available permissions can be found - * here. - * - * @deprecated Please use {@link #setScopes(List)} instead. - */ - @Deprecated - @NonNull - public GitHubBuilder setPermissions(@NonNull List permissions) { - setScopes(permissions); - return this; - } - } - - /** - * {@link IdpConfig} builder for the Apple provider. - */ - public static final class AppleBuilder extends GenericOAuthProviderBuilder { - private static final String PROVIDER_NAME = "Apple"; - - public AppleBuilder() { - super(APPLE_PROVIDER, PROVIDER_NAME, R.layout.fui_idp_button_apple); - } - } - - /** - * {@link IdpConfig} builder for the Microsoft provider. - */ - public static final class MicrosoftBuilder extends GenericOAuthProviderBuilder { - private static final String PROVIDER_NAME = "Microsoft"; - - public MicrosoftBuilder() { - super(MICROSOFT_PROVIDER, PROVIDER_NAME, R.layout.fui_idp_button_microsoft); - } - } - - /** - * {@link IdpConfig} builder for the Yahoo provider. - */ - public static final class YahooBuilder extends GenericOAuthProviderBuilder { - private static final String PROVIDER_NAME = "Yahoo"; - - public YahooBuilder() { - super(YAHOO_PROVIDER, PROVIDER_NAME, R.layout.fui_idp_button_yahoo); - } - } - - /** - * {@link IdpConfig} builder for a Generic OAuth provider. - */ - public static class GenericOAuthProviderBuilder extends Builder { - - public GenericOAuthProviderBuilder(@NonNull String providerId, - @NonNull String providerName, - int buttonId) { - super(providerId); - - Preconditions.checkNotNull(providerId, "The provider ID cannot be null."); - Preconditions.checkNotNull(providerName, "The provider name cannot be null."); - - getParams().putString( - ExtraConstants.GENERIC_OAUTH_PROVIDER_ID, providerId); - getParams().putString( - ExtraConstants.GENERIC_OAUTH_PROVIDER_NAME, providerName); - getParams().putInt( - ExtraConstants.GENERIC_OAUTH_BUTTON_ID, buttonId); - - } - - @NonNull - public GenericOAuthProviderBuilder setScopes(@NonNull List scopes) { - getParams().putStringArrayList( - ExtraConstants.GENERIC_OAUTH_SCOPES, new ArrayList<>(scopes)); - return this; - } - - @NonNull - public GenericOAuthProviderBuilder setCustomParameters( - @NonNull Map customParameters) { - getParams().putSerializable( - ExtraConstants.GENERIC_OAUTH_CUSTOM_PARAMETERS, - new HashMap<>(customParameters)); - return this; - } - } - } - - /** - * Base builder for both {@link SignInIntentBuilder}. - */ - @SuppressWarnings(value = "unchecked") - private abstract class AuthIntentBuilder { - final List mProviders = new ArrayList<>(); - IdpConfig mDefaultProvider = null; - int mLogo = NO_LOGO; - int mTheme = getDefaultTheme(); - String mTosUrl; - String mPrivacyPolicyUrl; - boolean mAlwaysShowProviderChoice = false; - boolean mLockOrientation = false; - boolean mEnableCredentials = true; - AuthMethodPickerLayout mAuthMethodPickerLayout = null; - ActionCodeSettings mPasswordSettings = null; - - /** - * Specifies the theme to use for the application flow. If no theme is specified, a - * default theme will be used. - */ - @NonNull - public T setTheme(@StyleRes int theme) { - mTheme = Preconditions.checkValidStyle( - mApp.getApplicationContext(), - theme, - "theme identifier is unknown or not a style definition"); - return (T) this; - } - - /** - * Specifies the logo to use for the {@link AuthMethodPickerActivity}. If no logo is - * specified, none will be used. - */ - @NonNull - public T setLogo(@DrawableRes int logo) { - mLogo = logo; - return (T) this; - } - - /** - * Specifies the terms-of-service URL for the application. - * - * @deprecated Please use {@link #setTosAndPrivacyPolicyUrls(String, String)} For the Tos - * link to be displayed a Privacy Policy url must also be provided. - */ - @NonNull - @Deprecated - public T setTosUrl(@Nullable String tosUrl) { - mTosUrl = tosUrl; - return (T) this; - } - - /** - * Specifies the privacy policy URL for the application. - * - * @deprecated Please use {@link #setTosAndPrivacyPolicyUrls(String, String)} For the - * Privacy Policy link to be displayed a Tos url must also be provided. - */ - @NonNull - @Deprecated - public T setPrivacyPolicyUrl(@Nullable String privacyPolicyUrl) { - mPrivacyPolicyUrl = privacyPolicyUrl; - return (T) this; - } - - @NonNull - public T setTosAndPrivacyPolicyUrls(@NonNull String tosUrl, - @NonNull String privacyPolicyUrl) { - Preconditions.checkNotNull(tosUrl, "tosUrl cannot be null"); - Preconditions.checkNotNull(privacyPolicyUrl, "privacyPolicyUrl cannot be null"); - mTosUrl = tosUrl; - mPrivacyPolicyUrl = privacyPolicyUrl; - return (T) this; - } - - /** - * Specifies the set of supported authentication providers. At least one provider must - * be specified. There may only be one instance of each provider. Anonymous provider cannot - * be the only provider specified. - *

- *

If no providers are explicitly specified by calling this method, then the email - * provider is the default supported provider. - * - * @param idpConfigs a list of {@link IdpConfig}s, where each {@link IdpConfig} contains the - * configuration parameters for the IDP. - * @throws IllegalStateException if anonymous provider is the only specified provider. - * @see IdpConfig - */ - @NonNull - public T setAvailableProviders(@NonNull List idpConfigs) { - Preconditions.checkNotNull(idpConfigs, "idpConfigs cannot be null"); - if (idpConfigs.size() == 1 && - idpConfigs.get(0).getProviderId().equals(ANONYMOUS_PROVIDER)) { - throw new IllegalStateException("Sign in as guest cannot be the only sign in " + - "method. In this case, sign the user in anonymously your self; " + - "no UI is needed."); - } - - mProviders.clear(); - - for (IdpConfig config : idpConfigs) { - if (mProviders.contains(config)) { - throw new IllegalArgumentException("Each provider can only be set once. " - + config.getProviderId() - + " was set twice."); - } else { - mProviders.add(config); - } - } - - return (T) this; - } - - /** - * Specifies the default authentication provider, bypassing the provider selection screen. - * The provider here must already be included via {@link #setAvailableProviders(List)}, and - * this method is incompatible with {@link #setAlwaysShowSignInMethodScreen(boolean)}. - * - * @param config the default {@link IdpConfig} to use. - */ - @NonNull - public T setDefaultProvider(@Nullable IdpConfig config) { - if (config != null) { - if (!mProviders.contains(config)) { - throw new IllegalStateException( - "Default provider not in available providers list."); - } - if (mAlwaysShowProviderChoice) { - throw new IllegalStateException( - "Can't set default provider and always show provider choice."); - } - } - mDefaultProvider = config; - return (T) this; - } - - /** - * Enables or disables the use of Credential Manager for Passwords credential selector - *

- *

Is enabled by default. - * - * @param enableCredentials enables credential selector before signup - */ - @NonNull - public T setCredentialManagerEnabled(boolean enableCredentials) { - mEnableCredentials = enableCredentials; - return (T) this; - } - - /** - * Set a custom layout for the AuthMethodPickerActivity screen. - * See {@link AuthMethodPickerLayout}. - * - * @param authMethodPickerLayout custom layout descriptor object. - */ - @NonNull - public T setAuthMethodPickerLayout(@NonNull AuthMethodPickerLayout authMethodPickerLayout) { - mAuthMethodPickerLayout = authMethodPickerLayout; - return (T) this; - } - - /** - * Forces the sign-in method choice screen to always show, even if there is only - * a single provider configured. - *

- *

This is false by default. - * - * @param alwaysShow if true, force the sign-in choice screen to show. - */ - @NonNull - public T setAlwaysShowSignInMethodScreen(boolean alwaysShow) { - if (alwaysShow && mDefaultProvider != null) { - throw new IllegalStateException( - "Can't show provider choice with a default provider."); - } - mAlwaysShowProviderChoice = alwaysShow; - return (T) this; - } - - /** - * Enable or disables the orientation for small devices to be locked in - * Portrait orientation - *

- *

This is false by default. - * - * @param lockOrientation if true, force the activities to be in Portrait orientation. - */ - @NonNull - public T setLockOrientation(boolean lockOrientation) { - mLockOrientation = lockOrientation; - return (T) this; - } - - /** - * Set custom settings for the RecoverPasswordActivity. - * - * @param passwordSettings to allow additional state via a continue URL. - */ - @NonNull - public T setResetPasswordSettings(ActionCodeSettings passwordSettings) { - mPasswordSettings = passwordSettings; - return (T) this; - } - - @CallSuper - @NonNull - public Intent build() { - if (mProviders.isEmpty()) { - mProviders.add(new IdpConfig.EmailBuilder().build()); - } - - return KickoffActivity.createIntent(mApp.getApplicationContext(), getFlowParams()); - } - - protected abstract FlowParameters getFlowParams(); - } - - /** - * Builder for the intent to start the user authentication flow. - */ - public final class SignInIntentBuilder extends AuthIntentBuilder { - - private String mEmailLink; - private boolean mEnableAnonymousUpgrade; - - private SignInIntentBuilder() { - super(); - } - - /** - * Specifies the email link to be used for sign in. When set, a sign in attempt will be - * made immediately. - */ - @NonNull - public SignInIntentBuilder setEmailLink(@NonNull final String emailLink) { - mEmailLink = emailLink; - return this; - } - - /** - * Enables upgrading anonymous accounts to full accounts during the sign-in flow. - * This is disabled by default. - * - * @throws IllegalStateException when you attempt to enable anonymous user upgrade - * without forcing the same device flow for email link sign in. - */ - @NonNull - public SignInIntentBuilder enableAnonymousUsersAutoUpgrade() { - mEnableAnonymousUpgrade = true; - validateEmailBuilderConfig(); - return this; - } - - private void validateEmailBuilderConfig() { - for (int i = 0; i < mProviders.size(); i++) { - IdpConfig config = mProviders.get(i); - if (config.getProviderId().equals(EMAIL_LINK_PROVIDER)) { - boolean emailLinkForceSameDevice = - config.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE, true); - if (!emailLinkForceSameDevice) { - throw new IllegalStateException("You must force the same device flow " + - "when using email link sign in with anonymous user upgrade"); - } - } - } - } - - @Override - protected FlowParameters getFlowParams() { - return new FlowParameters( - mApp.getName(), - mProviders, - mDefaultProvider, - mTheme, - mLogo, - mTosUrl, - mPrivacyPolicyUrl, - mEnableCredentials, - mEnableAnonymousUpgrade, - mAlwaysShowProviderChoice, - mLockOrientation, - mEmailLink, - mPasswordSettings, - mAuthMethodPickerLayout); - } - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/AuthUI.kt new file mode 100644 index 000000000..5dd9e0d0f --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/AuthUI.kt @@ -0,0 +1,889 @@ +/* + * 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 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.util.Log +import androidx.annotation.CallSuper +import androidx.annotation.DrawableRes +import androidx.annotation.NonNull +import androidx.annotation.Nullable +import androidx.annotation.RestrictTo +import androidx.annotation.StringDef +import androidx.annotation.StyleRes +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.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.Scope +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.firebase.FirebaseApp +import com.google.firebase.auth.ActionCodeSettings +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.GithubAuthProvider +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.PhoneAuthProvider +import com.google.firebase.auth.TwitterAuthProvider +import java.util.IdentityHashMap +import java.util.* +import com.google.android.gms.common.api.CommonStatusCodes + +class AuthUI private constructor(private val mApp: FirebaseApp) { + + private val mAuth: FirebaseAuth = FirebaseAuth.getInstance(mApp) + private var mEmulatorHost: String? = null + private var mEmulatorPort = -1 + + init { + try { + mAuth.setFirebaseUIVersion(BuildConfig.VERSION_NAME) + } catch (e: Exception) { + Log.e(TAG, "Couldn't set the FUI version.", e) + } + mAuth.useAppLanguage() + } + + fun getApp(): FirebaseApp = mApp + + fun getAuth(): FirebaseAuth = mAuth + + fun signOut(context: Context): Task { + val playServicesAvailable = GoogleApiUtils.isPlayServicesAvailable(context) + if (!playServicesAvailable) { + Log.w(TAG, "Google Play services not available during signOut") + } + return signOutIdps(context).continueWith { task -> + task.result // propagate exceptions if any. + mAuth.signOut() + null + } + } + + fun delete(context: Context): Task { + val currentUser = mAuth.currentUser + if (currentUser == null) { + return Tasks.forException( + FirebaseAuthInvalidUserException( + CommonStatusCodes.SIGN_IN_REQUIRED.toString(), + "No currently signed in user." + ) + ) + } + return signOutIdps(context).continueWithTask { task -> + task.result // propagate exception if any. + currentUser.delete() + } + } + + fun useEmulator(host: String, port: Int) { + Preconditions.checkArgument(port >= 0, "Port must be >= 0") + Preconditions.checkArgument(port <= 65535, "Port must be <= 65535") + mEmulatorHost = host + mEmulatorPort = port + + mAuth.useEmulator(host, port) + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun isUseEmulator(): Boolean = mEmulatorHost != null && mEmulatorPort >= 0 + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getEmulatorHost(): String? = mEmulatorHost + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getEmulatorPort(): Int = mEmulatorPort + + private fun signOutIdps(context: Context): Task { + if (ProviderAvailability.IS_FACEBOOK_AVAILABLE) { + LoginManager.getInstance().logOut() + } + return if (GoogleApiUtils.isPlayServicesAvailable(context)) { + GoogleSignIn.getClient(context, GoogleSignInOptions.DEFAULT_SIGN_IN).signOut() + } else { + Tasks.forResult(null) + } + } + + fun createSignInIntentBuilder(): SignInIntentBuilder { + return SignInIntentBuilder() + } + + @StringDef( + GoogleAuthProvider.PROVIDER_ID, + FacebookAuthProvider.PROVIDER_ID, + TwitterAuthProvider.PROVIDER_ID, + GithubAuthProvider.PROVIDER_ID, + EmailAuthProvider.PROVIDER_ID, + PhoneAuthProvider.PROVIDER_ID, + ANONYMOUS_PROVIDER, + EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD + ) + @Retention(AnnotationRetention.SOURCE) + annotation class SupportedProvider + + class IdpConfig private constructor(private val mProviderId: String, private val mParams: Bundle) : + Parcelable { + + val providerId: String + get() = mProviderId + + fun getParams(): Bundle = Bundle(mParams) + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(mProviderId) + parcel.writeBundle(mParams) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val config = other as IdpConfig + return mProviderId == config.mProviderId + } + + override fun hashCode(): Int = mProviderId.hashCode() + + override fun toString(): String { + return "IdpConfig{mProviderId='$mProviderId', mParams=$mParams}" + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): IdpConfig { + return IdpConfig(parcel) + } + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + private constructor(parcel: Parcel) : this( + parcel.readString() ?: "", + parcel.readBundle(IdpConfig::class.java.classLoader) ?: Bundle() + ) + + open class Builder(@NonNull @SupportedProvider providerId: String) { + protected val mParams: Bundle = Bundle() + protected var mProviderId: String = providerId + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected fun getParams(): Bundle = mParams + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected fun setProviderId(providerId: String) { + mProviderId = providerId + } + + @CallSuper + open fun build(): IdpConfig = IdpConfig(mProviderId, mParams) + } + + class EmailBuilder : Builder(EmailAuthProvider.PROVIDER_ID) { + fun setAllowNewAccounts(allow: Boolean): EmailBuilder { + getParams().putBoolean(ExtraConstants.ALLOW_NEW_EMAILS, allow) + return this + } + + fun setRequireName(requireName: Boolean): EmailBuilder { + getParams().putBoolean(ExtraConstants.REQUIRE_NAME, requireName) + return this + } + + fun enableEmailLinkSignIn(): EmailBuilder { + setProviderId(EMAIL_LINK_PROVIDER) + return this + } + + fun setActionCodeSettings(actionCodeSettings: ActionCodeSettings): EmailBuilder { + getParams().putParcelable(ExtraConstants.ACTION_CODE_SETTINGS, actionCodeSettings) + return this + } + + fun setForceSameDevice(): EmailBuilder { + getParams().putBoolean(ExtraConstants.FORCE_SAME_DEVICE, true) + return this + } + + fun setDefaultEmail(email: String): EmailBuilder { + getParams().putString(ExtraConstants.DEFAULT_EMAIL, email) + return this + } + + override fun build(): IdpConfig { + if (mProviderId == EMAIL_LINK_PROVIDER) { + val actionCodeSettings: ActionCodeSettings? = + getParams().getParcelable(ExtraConstants.ACTION_CODE_SETTINGS) + Preconditions.checkNotNull( + actionCodeSettings, + "ActionCodeSettings cannot be null when using email link sign in." + ) + if (!actionCodeSettings!!.canHandleCodeInApp()) { + throw IllegalStateException( + "You must set canHandleCodeInApp in your ActionCodeSettings to true for Email-Link Sign-in." + ) + } + } + return super.build() + } + } + + class PhoneBuilder : Builder(PhoneAuthProvider.PROVIDER_ID) { + fun setDefaultNumber(number: String): PhoneBuilder { + Preconditions.checkUnset( + getParams(), + "Cannot overwrite previously set phone number", + ExtraConstants.PHONE, + ExtraConstants.COUNTRY_ISO, + ExtraConstants.NATIONAL_NUMBER + ) + if (!PhoneNumberUtils.isValid(number)) { + throw IllegalStateException("Invalid phone number: $number") + } + getParams().putString(ExtraConstants.PHONE, number) + return this + } + + fun setDefaultNumber(iso: String, number: String): PhoneBuilder { + Preconditions.checkUnset( + getParams(), + "Cannot overwrite previously set phone number", + ExtraConstants.PHONE, + ExtraConstants.COUNTRY_ISO, + ExtraConstants.NATIONAL_NUMBER + ) + if (!PhoneNumberUtils.isValidIso(iso)) { + throw IllegalStateException("Invalid country iso: $iso") + } + getParams().putString(ExtraConstants.COUNTRY_ISO, iso) + getParams().putString(ExtraConstants.NATIONAL_NUMBER, number) + return this + } + + fun setDefaultCountryIso(iso: String): PhoneBuilder { + Preconditions.checkUnset( + getParams(), + "Cannot overwrite previously set phone number", + ExtraConstants.PHONE, + ExtraConstants.COUNTRY_ISO, + ExtraConstants.NATIONAL_NUMBER + ) + if (!PhoneNumberUtils.isValidIso(iso)) { + throw IllegalStateException("Invalid country iso: $iso") + } + getParams().putString( + ExtraConstants.COUNTRY_ISO, + iso.uppercase(Locale.getDefault()) + ) + return this + } + + fun setAllowedCountries(countries: List): PhoneBuilder { + if (getParams().containsKey(ExtraConstants.BLOCKLISTED_COUNTRIES)) { + throw IllegalStateException( + "You can either allowlist or blocklist country codes for phone authentication." + ) + } + val message = + "Invalid argument: Only non-%s allowlists are valid. To specify no allowlist, do not call this method." + Preconditions.checkNotNull(countries, String.format(message, "null")) + Preconditions.checkArgument(countries.isNotEmpty(), String.format(message, "empty")) + addCountriesToBundle(countries, ExtraConstants.ALLOWLISTED_COUNTRIES) + return this + } + + fun setBlockedCountries(countries: List): PhoneBuilder { + if (getParams().containsKey(ExtraConstants.ALLOWLISTED_COUNTRIES)) { + throw IllegalStateException( + "You can either allowlist or blocklist country codes for phone authentication." + ) + } + val message = + "Invalid argument: Only non-%s blocklists are valid. To specify no blocklist, do not call this method." + Preconditions.checkNotNull(countries, String.format(message, "null")) + Preconditions.checkArgument(countries.isNotEmpty(), String.format(message, "empty")) + addCountriesToBundle(countries, ExtraConstants.BLOCKLISTED_COUNTRIES) + return this + } + + override fun build(): IdpConfig { + validateInputs() + return super.build() + } + + private fun addCountriesToBundle(countryIsos: List, countryIsoType: String) { + val uppercaseCodes = ArrayList() + for (code in countryIsos) { + uppercaseCodes.add(code.uppercase(Locale.getDefault())) + } + getParams().putStringArrayList(countryIsoType, uppercaseCodes) + } + + private fun validateInputs() { + val allowedCountries = getParams().getStringArrayList(ExtraConstants.ALLOWLISTED_COUNTRIES) + val blockedCountries = getParams().getStringArrayList(ExtraConstants.BLOCKLISTED_COUNTRIES) + if (allowedCountries != null && blockedCountries != null) { + throw IllegalStateException( + "You can either allowlist or blocked country codes for phone authentication." + ) + } else if (allowedCountries != null) { + validateInputs(allowedCountries, true) + } else if (blockedCountries != null) { + validateInputs(blockedCountries, false) + } + } + + private fun validateInputs(countries: List, allowed: Boolean) { + validateCountryInput(countries) + validateDefaultCountryInput(countries, allowed) + } + + private fun validateCountryInput(codes: List) { + for (code in codes) { + if (!PhoneNumberUtils.isValidIso(code) && !PhoneNumberUtils.isValid(code)) { + throw IllegalArgumentException( + "Invalid input: You must provide a valid country iso (alpha-2) or code (e-164). e.g. 'us' or '+1'." + ) + } + } + } + + private fun validateDefaultCountryInput(codes: List, allowed: Boolean) { + if (getParams().containsKey(ExtraConstants.COUNTRY_ISO) || + getParams().containsKey(ExtraConstants.PHONE) + ) { + if (!validateDefaultCountryIso(codes, allowed) || + !validateDefaultPhoneIsos(codes, allowed) + ) { + throw IllegalArgumentException( + "Invalid default country iso. Make sure it is either part of the allowed list or that you haven't blocked it." + ) + } + } + } + + private fun validateDefaultCountryIso(codes: List, allowed: Boolean): Boolean { + val defaultIso = getDefaultIso() + return isValidDefaultIso(codes, defaultIso, allowed) + } + + private fun validateDefaultPhoneIsos(codes: List, allowed: Boolean): Boolean { + val phoneIsos = getPhoneIsosFromCode() + for (iso in phoneIsos) { + if (isValidDefaultIso(codes, iso, allowed)) { + return true + } + } + return phoneIsos.isEmpty() + } + + private fun isValidDefaultIso(codes: List, iso: String?, allowed: Boolean): Boolean { + if (iso == null) return true + val containsIso = containsCountryIso(codes, iso) + return (containsIso && allowed) || (!containsIso && !allowed) + } + + private fun containsCountryIso(codes: List, iso: String): Boolean { + val isoUpper = iso.uppercase(Locale.getDefault()) + for (code in codes) { + if (PhoneNumberUtils.isValidIso(code)) { + if (code.equals(isoUpper, ignoreCase = true)) { + return true + } + } else { + val isos = PhoneNumberUtils.getCountryIsosFromCountryCode(code) + if (isos?.contains(isoUpper) == true) { + return true + } + } + } + return false + } + + private fun getPhoneIsosFromCode(): List { + val isos = ArrayList() + val phone = getParams().getString(ExtraConstants.PHONE) + if (phone != null && phone.startsWith("+")) { + val countryCode = "+" + PhoneNumberUtils.getPhoneNumber(phone).countryCode + val isosToAdd = PhoneNumberUtils.getCountryIsosFromCountryCode(countryCode) + if (isosToAdd != null) { + isos.addAll(isosToAdd) + } + } + return isos + } + + private fun getDefaultIso(): String? { + return if (getParams().containsKey(ExtraConstants.COUNTRY_ISO)) + getParams().getString(ExtraConstants.COUNTRY_ISO) + else null + } + } + + class GoogleBuilder : Builder(GoogleAuthProvider.PROVIDER_ID) { + private fun validateWebClientId() { + Preconditions.checkConfigured( + AuthUI.getApplicationContext(), + "Check your google-services plugin configuration, the default_web_client_id string wasn't populated.", + R.string.default_web_client_id + ) + } + + fun setScopes(scopes: List): GoogleBuilder { + val builder = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + for (scope in scopes) { + builder.requestScopes(Scope(scope)) + } + return setSignInOptions(builder.build()) + } + + fun setSignInOptions(options: GoogleSignInOptions): GoogleBuilder { + Preconditions.checkUnset( + getParams(), + "Cannot overwrite previously set sign-in options.", + ExtraConstants.GOOGLE_SIGN_IN_OPTIONS + ) + val builder = GoogleSignInOptions.Builder(options) + var clientId = options.serverClientId + if (clientId == null) { + validateWebClientId() + clientId = AuthUI.getApplicationContext().getString(R.string.default_web_client_id) + } + var hasEmailScope = false + for (s in options.scopes) { + if ("email" == s.scopeUri) { + hasEmailScope = true + break + } + } + if (!hasEmailScope) { + Log.w( + TAG, + "The GoogleSignInOptions passed to setSignInOptions does not request the 'email' scope. In most cases this is a mistake! Call requestEmail() on the GoogleSignInOptions object." + ) + } + builder.requestIdToken(clientId) + getParams().putParcelable(ExtraConstants.GOOGLE_SIGN_IN_OPTIONS, builder.build()) + return this + } + + override fun build(): IdpConfig { + if (!getParams().containsKey(ExtraConstants.GOOGLE_SIGN_IN_OPTIONS)) { + validateWebClientId() + setScopes(Collections.emptyList()) + } + return super.build() + } + } + + class FacebookBuilder : Builder(FacebookAuthProvider.PROVIDER_ID) { + init { + if (!ProviderAvailability.IS_FACEBOOK_AVAILABLE) { + throw RuntimeException( + "Facebook provider cannot be configured without dependency. Did you forget to add 'com.facebook.android:facebook-login:VERSION' dependency?" + ) + } + Preconditions.checkConfigured( + AuthUI.getApplicationContext(), + "Facebook provider unconfigured. Make sure to add a `facebook_application_id` string. See the docs for more info: " + + "https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#facebook", + R.string.facebook_application_id + ) + if (AuthUI.getApplicationContext().getString(R.string.facebook_login_protocol_scheme) == "fbYOUR_APP_ID") { + Log.w(TAG, "Facebook provider unconfigured for Chrome Custom Tabs.") + } + } + + fun setPermissions(permissions: List): FacebookBuilder { + getParams().putStringArrayList( + ExtraConstants.FACEBOOK_PERMISSIONS, + ArrayList(permissions) + ) + return this + } + } + + class AnonymousBuilder : Builder(ANONYMOUS_PROVIDER) + + class TwitterBuilder : GenericOAuthProviderBuilder( + TwitterAuthProvider.PROVIDER_ID, + "Twitter", + R.layout.fui_idp_button_twitter + ) + + class GitHubBuilder : GenericOAuthProviderBuilder( + GithubAuthProvider.PROVIDER_ID, + "Github", + R.layout.fui_idp_button_github + ) { + @Deprecated("Please use setScopes(List) instead.") + fun setPermissions(permissions: List): GitHubBuilder { + setScopes(permissions) + return this + } + } + + class AppleBuilder : GenericOAuthProviderBuilder( + APPLE_PROVIDER, + "Apple", + R.layout.fui_idp_button_apple + ) + + class MicrosoftBuilder : GenericOAuthProviderBuilder( + MICROSOFT_PROVIDER, + "Microsoft", + R.layout.fui_idp_button_microsoft + ) + + class YahooBuilder : GenericOAuthProviderBuilder( + YAHOO_PROVIDER, + "Yahoo", + R.layout.fui_idp_button_yahoo + ) + + open class GenericOAuthProviderBuilder(providerId: String, providerName: String, buttonId: Int) : + Builder(providerId) { + init { + Preconditions.checkNotNull(providerId, "The provider ID cannot be null.") + Preconditions.checkNotNull(providerName, "The provider name cannot be null.") + getParams().putString(ExtraConstants.GENERIC_OAUTH_PROVIDER_ID, providerId) + getParams().putString(ExtraConstants.GENERIC_OAUTH_PROVIDER_NAME, providerName) + getParams().putInt(ExtraConstants.GENERIC_OAUTH_BUTTON_ID, buttonId) + } + + fun setScopes(scopes: List): GenericOAuthProviderBuilder { + getParams().putStringArrayList( + ExtraConstants.GENERIC_OAUTH_SCOPES, + ArrayList(scopes) + ) + return this + } + + fun setCustomParameters(customParameters: Map): GenericOAuthProviderBuilder { + getParams().putSerializable( + ExtraConstants.GENERIC_OAUTH_CUSTOM_PARAMETERS, + HashMap(customParameters) + ) + return this + } + } + } + + @Suppress("UNCHECKED_CAST") + public abstract inner class AuthIntentBuilder> { + val mProviders: MutableList = ArrayList() + var mDefaultProvider: IdpConfig? = null + var mLogo: Int = NO_LOGO + var mTheme: Int = getDefaultTheme() + var mTosUrl: String? = null + var mPrivacyPolicyUrl: String? = null + var mAlwaysShowProviderChoice = false + var mLockOrientation = false + var mEnableCredentials = true + var mAuthMethodPickerLayout: AuthMethodPickerLayout? = null + var mPasswordSettings: ActionCodeSettings? = null + + fun setTheme(@StyleRes theme: Int): T { + mTheme = Preconditions.checkValidStyle( + mApp.applicationContext, + theme, + "theme identifier is unknown or not a style definition" + ) + return this as T + } + + fun setLogo(@DrawableRes logo: Int): T { + mLogo = logo + return this as T + } + + @Deprecated("Please use setTosAndPrivacyPolicyUrls(String, String)") + fun setTosUrl(tosUrl: String?): T { + mTosUrl = tosUrl + return this as T + } + + @Deprecated("Please use setTosAndPrivacyPolicyUrls(String, String)") + fun setPrivacyPolicyUrl(privacyPolicyUrl: String?): T { + mPrivacyPolicyUrl = privacyPolicyUrl + return this as T + } + + fun setTosAndPrivacyPolicyUrls(tosUrl: String, privacyPolicyUrl: String): T { + Preconditions.checkNotNull(tosUrl, "tosUrl cannot be null") + Preconditions.checkNotNull(privacyPolicyUrl, "privacyPolicyUrl cannot be null") + mTosUrl = tosUrl + mPrivacyPolicyUrl = privacyPolicyUrl + return this as T + } + + fun setAvailableProviders(idpConfigs: List): T { + Preconditions.checkNotNull(idpConfigs, "idpConfigs cannot be null") + if (idpConfigs.size == 1 && idpConfigs[0].providerId == ANONYMOUS_PROVIDER) { + throw IllegalStateException( + "Sign in as guest cannot be the only sign in method. In this case, sign the user in anonymously your self; no UI is needed." + ) + } + mProviders.clear() + for (config in idpConfigs) { + if (mProviders.contains(config)) { + throw IllegalArgumentException("Each provider can only be set once. " + + "${config.providerId} was set twice.") + } else { + mProviders.add(config) + } + } + return this as T + } + + fun setDefaultProvider(config: IdpConfig?): T { + if (config != null) { + if (!mProviders.contains(config)) { + throw IllegalStateException("Default provider not in available providers list.") + } + if (mAlwaysShowProviderChoice) { + throw IllegalStateException("Can't set default provider and always show provider choice.") + } + } + mDefaultProvider = config + return this as T + } + + fun setCredentialManagerEnabled(enableCredentials: Boolean): T { + mEnableCredentials = enableCredentials + return this as T + } + + fun setAuthMethodPickerLayout(authMethodPickerLayout: AuthMethodPickerLayout): T { + mAuthMethodPickerLayout = authMethodPickerLayout + return this as T + } + + fun setAlwaysShowSignInMethodScreen(alwaysShow: Boolean): T { + if (alwaysShow && mDefaultProvider != null) { + throw IllegalStateException("Can't show provider choice with a default provider.") + } + mAlwaysShowProviderChoice = alwaysShow + return this as T + } + + fun setLockOrientation(lockOrientation: Boolean): T { + mLockOrientation = lockOrientation + return this as T + } + + fun setResetPasswordSettings(passwordSettings: ActionCodeSettings): T { + mPasswordSettings = passwordSettings + return this as T + } + + @CallSuper + open fun build(): Intent { + if (mProviders.isEmpty()) { + mProviders.add(IdpConfig.EmailBuilder().build()) + } + return KickoffActivity.createIntent(mApp.applicationContext, getFlowParams()) + } + + protected abstract fun getFlowParams(): FlowParameters + } + + inner class SignInIntentBuilder : AuthIntentBuilder() { + private var mEmailLink: String? = null + private var mEnableAnonymousUpgrade = false + + fun setEmailLink(emailLink: String): SignInIntentBuilder { + mEmailLink = emailLink + return this + } + + fun enableAnonymousUsersAutoUpgrade(): SignInIntentBuilder { + mEnableAnonymousUpgrade = true + validateEmailBuilderConfig() + return this + } + + private fun validateEmailBuilderConfig() { + for (config in mProviders) { + if (config.providerId == EMAIL_LINK_PROVIDER) { + val emailLinkForceSameDevice = + config.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE, true) + if (!emailLinkForceSameDevice) { + throw IllegalStateException( + "You must force the same device flow when using email link sign in with anonymous user upgrade" + ) + } + } + } + } + + override fun getFlowParams(): FlowParameters { + return FlowParameters( + mApp.name, + mProviders, + mDefaultProvider, + mTheme, + mLogo, + mTosUrl, + mPrivacyPolicyUrl, + mEnableCredentials, + mEnableAnonymousUpgrade, + mAlwaysShowProviderChoice, + mLockOrientation, + mEmailLink, + mPasswordSettings, + mAuthMethodPickerLayout + ) + } + } + + companion object { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + const val TAG = "AuthUI" + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + const val ANONYMOUS_PROVIDER = "anonymous" + const val EMAIL_LINK_PROVIDER = EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD + + const val MICROSOFT_PROVIDER = "microsoft.com" + const val YAHOO_PROVIDER = "yahoo.com" + const val APPLE_PROVIDER = "apple.com" + + const val NO_LOGO = -1 + + @JvmField + public val SUPPORTED_PROVIDERS: Set = setOf( + GoogleAuthProvider.PROVIDER_ID, + FacebookAuthProvider.PROVIDER_ID, + TwitterAuthProvider.PROVIDER_ID, + GithubAuthProvider.PROVIDER_ID, + EmailAuthProvider.PROVIDER_ID, + PhoneAuthProvider.PROVIDER_ID, + ANONYMOUS_PROVIDER, + EMAIL_LINK_PROVIDER + ) + + @JvmField + public val SUPPORTED_OAUTH_PROVIDERS: Set = setOf( + MICROSOFT_PROVIDER, + YAHOO_PROVIDER, + APPLE_PROVIDER, + TwitterAuthProvider.PROVIDER_ID, + GithubAuthProvider.PROVIDER_ID + ) + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + val SOCIAL_PROVIDERS: Set = setOf( + GoogleAuthProvider.PROVIDER_ID, + FacebookAuthProvider.PROVIDER_ID + ) + + @JvmStatic + fun isSocialProvider(providerId: String): Boolean { + return SOCIAL_PROVIDERS.contains(providerId) + } + + @JvmStatic + fun isSupportedProvider(providerId: String): Boolean { + return SUPPORTED_PROVIDERS.contains(providerId) + } + + @JvmStatic + fun isSupportedOAuthProvider(providerId: String): Boolean { + return SUPPORTED_OAUTH_PROVIDERS.contains(providerId) + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + const val UNCONFIGURED_CONFIG_VALUE = "CHANGE-ME" + + private val INSTANCES: IdentityHashMap = IdentityHashMap() + + @JvmStatic + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getApplicationContext(): Context { + return sApplicationContext ?: throw IllegalStateException("Application context not set") + } + + @JvmStatic + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun setApplicationContext(context: Context) { + sApplicationContext = Preconditions.checkNotNull(context, "App context cannot be null.").applicationContext + } + + @JvmStatic + fun getInstance(): AuthUI { + return getInstance(FirebaseApp.getInstance()) + } + + @JvmStatic + fun getInstance(appName: String): AuthUI { + return getInstance(FirebaseApp.getInstance(appName)) + } + + @JvmStatic + fun getInstance(app: FirebaseApp): AuthUI { + val releaseUrl = "https://github.com/firebase/FirebaseUI-Android/releases/tag/6.2.0" + val devWarning = "Beginning with FirebaseUI 6.2.0 you no longer need to include %s to sign in with %s. Go to %s for more information" + if (ProviderAvailability.IS_TWITTER_AVAILABLE) { + Log.w(TAG, String.format(devWarning, "the TwitterKit SDK", "Twitter", releaseUrl)) + } + if (ProviderAvailability.IS_GITHUB_AVAILABLE) { + Log.w(TAG, String.format(devWarning, "com.firebaseui:firebase-ui-auth-github", "GitHub", releaseUrl)) + } + synchronized(INSTANCES) { + var authUi = INSTANCES[app] + if (authUi == null) { + authUi = AuthUI(app) + INSTANCES[app] = authUi + } + return authUi + } + } + + @JvmStatic + fun canHandleIntent(intent: Intent?): Boolean { + if (intent == null || intent.data == null) { + return false + } + val link = intent.data.toString() + return FirebaseAuth.getInstance().isSignInWithEmailLink(link) + } + + @StyleRes + @JvmStatic + fun getDefaultTheme(): Int = R.style.FirebaseUI_DefaultMaterialTheme + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + private var sApplicationContext: Context? = null + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/ErrorCodes.java b/auth/src/main/java/com/firebase/ui/auth/ErrorCodes.java deleted file mode 100644 index a39869d6e..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/ErrorCodes.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.firebase.ui.auth; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; - -/** - * Error codes for failed sign-in attempts. - */ -public final class ErrorCodes { - /** - * An unknown error has occurred. - */ - public static final int UNKNOWN_ERROR = 0; - /** - * Sign in failed due to lack of network connection. - */ - public static final int NO_NETWORK = 1; - /** - * A required update to Play Services was cancelled by the user. - */ - public static final int PLAY_SERVICES_UPDATE_CANCELLED = 2; - /** - * A sign-in operation couldn't be completed due to a developer error. - */ - public static final int DEVELOPER_ERROR = 3; - /** - * An external sign-in provider error occurred. - */ - public static final int PROVIDER_ERROR = 4; - /** - * Anonymous account linking failed. - */ - public static final int ANONYMOUS_UPGRADE_MERGE_CONFLICT = 5; - /** - * Signing in with a different email in the WelcomeBackIdp flow or email link flow. - */ - public static final int EMAIL_MISMATCH_ERROR = 6; - /** - * Attempting to sign in with an invalid email link. - */ - public static final int INVALID_EMAIL_LINK_ERROR = 7; - - /** - * Attempting to open an email link from a different device. - */ - public static final int EMAIL_LINK_WRONG_DEVICE_ERROR = 8; - - /** - * We need to prompt the user for their email. - * */ - public static final int EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR = 9; - - /** - * Cross device linking flow - we need to ask the user if they want to continue linking or - * just sign in. - * */ - public static final int EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR = 10; - - /** - * Attempting to open an email link from the same device, with anonymous upgrade enabled, - * but the underlying anonymous user has been changed. - */ - public static final int EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR = 11; - - /** - * Attempting to auth with account that is currently disabled in the Firebase console. - */ - public static final int ERROR_USER_DISABLED = 12; - - /** - * Recoverable error occurred during the Generic IDP flow. - */ - public static final int ERROR_GENERIC_IDP_RECOVERABLE_ERROR = 13; - - private ErrorCodes() { - throw new AssertionError("No instance for you!"); - } - - @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static String toFriendlyMessage(@Code int code) { - switch (code) { - case UNKNOWN_ERROR: - return "Unknown error"; - case NO_NETWORK: - return "No internet connection"; - case PLAY_SERVICES_UPDATE_CANCELLED: - return "Play Services update cancelled"; - case DEVELOPER_ERROR: - return "Developer error"; - case PROVIDER_ERROR: - return "Provider error"; - case ANONYMOUS_UPGRADE_MERGE_CONFLICT: - return "User account merge conflict"; - case EMAIL_MISMATCH_ERROR: - return "You are are attempting to sign in a different email than previously " + - "provided"; - case INVALID_EMAIL_LINK_ERROR: - return "You are are attempting to sign in with an invalid email link"; - case EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR: - return "Please enter your email to continue signing in"; - case EMAIL_LINK_WRONG_DEVICE_ERROR: - return "You must open the email link on the same device."; - case EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR: - return "You must determine if you want to continue linking or complete the sign in"; - case EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR: - return "The session associated with this sign-in request has either expired or " + - "was cleared"; - case ERROR_USER_DISABLED: - return "The user account has been disabled by an administrator."; - case ERROR_GENERIC_IDP_RECOVERABLE_ERROR: - return "Generic IDP recoverable error."; - default: - throw new IllegalArgumentException("Unknown code: " + code); - } - } - - /** - * Valid codes that can be returned from {@link FirebaseUiException#getErrorCode()}. - */ - @IntDef({ - UNKNOWN_ERROR, - NO_NETWORK, - PLAY_SERVICES_UPDATE_CANCELLED, - DEVELOPER_ERROR, - PROVIDER_ERROR, - ANONYMOUS_UPGRADE_MERGE_CONFLICT, - EMAIL_MISMATCH_ERROR, - INVALID_EMAIL_LINK_ERROR, - EMAIL_LINK_WRONG_DEVICE_ERROR, - EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR, - EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR, - EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR, - ERROR_USER_DISABLED, - ERROR_GENERIC_IDP_RECOVERABLE_ERROR - }) - @Retention(RetentionPolicy.SOURCE) - public @interface Code { - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/ErrorCodes.kt b/auth/src/main/java/com/firebase/ui/auth/ErrorCodes.kt new file mode 100644 index 000000000..a37746277 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ErrorCodes.kt @@ -0,0 +1,124 @@ +package com.firebase.ui.auth + +import androidx.annotation.RestrictTo +import androidx.annotation.IntDef +import kotlin.jvm.JvmStatic + +/** + * Error codes for failed sign-in attempts. + */ +object ErrorCodes { + /** + * Valid codes that can be returned from FirebaseUiException.getErrorCode(). + */ + @Retention(AnnotationRetention.SOURCE) + @IntDef( + UNKNOWN_ERROR, + NO_NETWORK, + PLAY_SERVICES_UPDATE_CANCELLED, + DEVELOPER_ERROR, + PROVIDER_ERROR, + ANONYMOUS_UPGRADE_MERGE_CONFLICT, + EMAIL_MISMATCH_ERROR, + INVALID_EMAIL_LINK_ERROR, + EMAIL_LINK_WRONG_DEVICE_ERROR, + EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR, + EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR, + EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR, + ERROR_USER_DISABLED, + ERROR_GENERIC_IDP_RECOVERABLE_ERROR + ) + annotation class Code + + /** + * An unknown error has occurred. + */ + const val UNKNOWN_ERROR = 0 + + /** + * Sign in failed due to lack of network connection. + */ + const val NO_NETWORK = 1 + + /** + * A required update to Play Services was cancelled by the user. + */ + const val PLAY_SERVICES_UPDATE_CANCELLED = 2 + + /** + * A sign-in operation couldn't be completed due to a developer error. + */ + const val DEVELOPER_ERROR = 3 + + /** + * An external sign-in provider error occurred. + */ + const val PROVIDER_ERROR = 4 + + /** + * Anonymous account linking failed. + */ + const val ANONYMOUS_UPGRADE_MERGE_CONFLICT = 5 + + /** + * Signing in with a different email in the WelcomeBackIdp flow or email link flow. + */ + const val EMAIL_MISMATCH_ERROR = 6 + + /** + * Attempting to sign in with an invalid email link. + */ + const val INVALID_EMAIL_LINK_ERROR = 7 + + /** + * Attempting to open an email link from a different device. + */ + const val EMAIL_LINK_WRONG_DEVICE_ERROR = 8 + + /** + * We need to prompt the user for their email. + */ + const val EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR = 9 + + /** + * Cross device linking flow - we need to ask the user if they want to continue linking or + * just sign in. + */ + const val EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR = 10 + + /** + * Attempting to open an email link from the same device, with anonymous upgrade enabled, + * but the underlying anonymous user has been changed. + */ + const val EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR = 11 + + /** + * Attempting to auth with account that is currently disabled in the Firebase console. + */ + const val ERROR_USER_DISABLED = 12 + + /** + * Recoverable error occurred during the Generic IDP flow. + */ + const val ERROR_GENERIC_IDP_RECOVERABLE_ERROR = 13 + + @JvmStatic + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun toFriendlyMessage(@Code code: Int): String = when (code) { + UNKNOWN_ERROR -> "Unknown error" + NO_NETWORK -> "No internet connection" + PLAY_SERVICES_UPDATE_CANCELLED -> "Play Services update cancelled" + DEVELOPER_ERROR -> "Developer error" + PROVIDER_ERROR -> "Provider error" + ANONYMOUS_UPGRADE_MERGE_CONFLICT -> "User account merge conflict" + EMAIL_MISMATCH_ERROR -> "You are are attempting to sign in a different email than previously provided" + INVALID_EMAIL_LINK_ERROR -> "You are are attempting to sign in with an invalid email link" + EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR -> "Please enter your email to continue signing in" + EMAIL_LINK_WRONG_DEVICE_ERROR -> "You must open the email link on the same device." + EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR -> "You must determine if you want to continue linking or complete the sign in" + EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR -> "The session associated with this sign-in request has either expired or was cleared" + ERROR_USER_DISABLED -> "The user account has been disabled by an administrator." + ERROR_GENERIC_IDP_RECOVERABLE_ERROR -> "Generic IDP recoverable error." + else -> throw IllegalArgumentException("Unknown code: $code") + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java b/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java index 337188acc..d658c4829 100644 --- a/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java +++ b/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java @@ -385,7 +385,7 @@ public IdpResponse build() { String providerId = mUser.getProviderId(); - if (AuthUI.SOCIAL_PROVIDERS.contains(providerId) && TextUtils.isEmpty(mToken)) { + if (AuthUI.isSocialProvider(providerId) && TextUtils.isEmpty(mToken)) { throw new IllegalStateException( "Token cannot be null when using a non-email provider."); } diff --git a/auth/src/main/java/com/firebase/ui/auth/data/client/AuthUiInitProvider.java b/auth/src/main/java/com/firebase/ui/auth/data/client/AuthUiInitProvider.java deleted file mode 100644 index d90e875d6..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/data/client/AuthUiInitProvider.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.firebase.ui.auth.data.client; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.ProviderInfo; -import android.database.Cursor; -import android.net.Uri; - -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.util.Preconditions; - -import androidx.annotation.RestrictTo; - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class AuthUiInitProvider extends ContentProvider { - @Override - public void attachInfo(Context context, ProviderInfo info) { - Preconditions.checkNotNull(info, "AuthUiInitProvider ProviderInfo cannot be null."); - if ("com.firebase.ui.auth.authuiinitprovider".equals(info.authority)) { - throw new IllegalStateException("Incorrect provider authority in manifest. Most" + - " likely due to a missing applicationId variable in application's build.gradle."); - } else { - super.attachInfo(context, info); - } - } - - @Override - public boolean onCreate() { - AuthUI.setApplicationContext(getContext()); - return false; - } - - @Override - public Cursor query(Uri uri, - String[] projection, - String selection, - String[] selectionArgs, - String sortOrder) { - return null; - } - - @Override - public String getType(Uri uri) { - return null; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - return 0; - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/data/client/AuthUiInitProvider.kt b/auth/src/main/java/com/firebase/ui/auth/data/client/AuthUiInitProvider.kt new file mode 100644 index 000000000..028659082 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/data/client/AuthUiInitProvider.kt @@ -0,0 +1,54 @@ +package com.firebase.ui.auth.data.client + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.net.Uri +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.util.Preconditions +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class AuthUiInitProvider : ContentProvider() { + + override fun attachInfo(context: Context, info: ProviderInfo) { + Preconditions.checkNotNull(info, "AuthUiInitProvider ProviderInfo cannot be null.") + if ("com.firebase.ui.auth.authuiinitprovider" == info.authority) { + throw IllegalStateException( + "Incorrect provider authority in manifest. Most likely due to a missing " + + "applicationId variable in application's build.gradle." + ) + } else { + super.attachInfo(context, info) + } + } + + override fun onCreate(): Boolean { + val context = context ?: throw IllegalStateException("Context cannot be null") + AuthUI.setApplicationContext(context) + return false + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String? = null + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int = 0 +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.java b/auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.java deleted file mode 100644 index d245146eb..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2015 Twitter, Inc. - * - * 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. - * - * Modifications copyright (C) 2017 Google Inc - * - */ -package com.firebase.ui.auth.data.model; - -import android.os.Parcel; -import android.os.Parcelable; - -import java.text.Collator; -import java.util.Locale; - -import androidx.annotation.RestrictTo; - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public final class CountryInfo implements Comparable, Parcelable { - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public CountryInfo createFromParcel(Parcel source) { - return new CountryInfo(source); - } - - @Override - public CountryInfo[] newArray(int size) { - return new CountryInfo[size]; - } - }; - - private final Collator mCollator; - private final Locale mLocale; - private final int mCountryCode; - - public CountryInfo(Locale locale, int countryCode) { - mCollator = Collator.getInstance(Locale.getDefault()); - mCollator.setStrength(Collator.PRIMARY); - mLocale = locale; - mCountryCode = countryCode; - } - - protected CountryInfo(Parcel in) { - mCollator = Collator.getInstance(Locale.getDefault()); - mCollator.setStrength(Collator.PRIMARY); - - mLocale = (Locale) in.readSerializable(); - mCountryCode = in.readInt(); - } - - public static String localeToEmoji(Locale locale) { - String countryCode = locale.getCountry(); - // 0x41 is Letter A - // 0x1F1E6 is Regional Indicator Symbol Letter A - // Example : - // firstLetter U => 20 + 0x1F1E6 - // secondLetter S => 18 + 0x1F1E6 - // See: https://en.wikipedia.org/wiki/Regional_Indicator_Symbol - int firstLetter = Character.codePointAt(countryCode, 0) - 0x41 + 0x1F1E6; - int secondLetter = Character.codePointAt(countryCode, 1) - 0x41 + 0x1F1E6; - return new String(Character.toChars(firstLetter)) + new String(Character.toChars - (secondLetter)); - } - - public Locale getLocale() { - return mLocale; - } - - public int getCountryCode() { - return mCountryCode; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final CountryInfo that = (CountryInfo) o; - - return mCountryCode == that.mCountryCode - && (mLocale != null ? mLocale.equals(that.mLocale) : that.mLocale == null); - } - - @Override - public int hashCode() { - int result = mLocale != null ? mLocale.hashCode() : 0; - result = 31 * result + mCountryCode; - return result; - } - - @Override - public String toString() { - return localeToEmoji(mLocale) + " " + mLocale.getDisplayCountry() + " +" + mCountryCode; - } - - public String toShortString() { - return localeToEmoji(mLocale) + " +" + mCountryCode; - } - - @Override - public int compareTo(CountryInfo info) { - Locale defaultLocale = Locale.getDefault(); - return mCollator.compare( - mLocale.getDisplayCountry().toUpperCase(defaultLocale), - info.mLocale.getDisplayCountry().toUpperCase(defaultLocale)); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeSerializable(mLocale); - dest.writeInt(mCountryCode); - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.kt b/auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.kt new file mode 100644 index 000000000..710fa79d9 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.kt @@ -0,0 +1,75 @@ +package com.firebase.ui.auth.data.model + +import android.os.Parcel +import android.os.Parcelable +import androidx.annotation.RestrictTo +import java.text.Collator +import java.util.Locale + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class CountryInfo(val locale: Locale?, val countryCode: Int) : Comparable, Parcelable { + + // Use a collator initialized to the default locale. + private val collator: Collator = Collator.getInstance(Locale.getDefault()).apply { + strength = Collator.PRIMARY + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): CountryInfo = CountryInfo(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + + fun localeToEmoji(locale: Locale?): String { + if (locale == null) return "" + val countryCode = locale.country + // 0x41 is Letter A, 0x1F1E6 is Regional Indicator Symbol Letter A. + // For example, for "US": 'U' => (0x55 - 0x41) + 0x1F1E6, 'S' => (0x53 - 0x41) + 0x1F1E6. + val firstLetter = Character.codePointAt(countryCode, 0) - 0x41 + 0x1F1E6 + val secondLetter = Character.codePointAt(countryCode, 1) - 0x41 + 0x1F1E6 + return String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter)) + } + } + + // Secondary constructor to recreate from a Parcel. + constructor(parcel: Parcel) : this( + parcel.readSerializable() as? Locale, + parcel.readInt() + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CountryInfo) return false + return countryCode == other.countryCode && locale == other.locale + } + + override fun hashCode(): Int { + var result = locale?.hashCode() ?: 0 + result = 31 * result + countryCode + return result + } + + override fun toString(): String { + return "${localeToEmoji(locale)} ${locale?.displayCountry ?: ""} +$countryCode" + } + + fun toShortString(): String { + return "${localeToEmoji(locale)} +$countryCode" + } + + override fun compareTo(other: CountryInfo): Int { + val defaultLocale = Locale.getDefault() + return collator.compare( + locale?.displayCountry?.uppercase(defaultLocale) ?: "", + other.locale?.displayCountry?.uppercase(defaultLocale) ?: "" + ) + } + + override fun describeContents(): Int = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeSerializable(locale) + dest.writeInt(countryCode) + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/data/model/FirebaseAuthUIAuthenticationResult.java b/auth/src/main/java/com/firebase/ui/auth/data/model/FirebaseAuthUIAuthenticationResult.java deleted file mode 100644 index eea7deabc..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/data/model/FirebaseAuthUIAuthenticationResult.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.firebase.ui.auth.data.model; - -import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract; -import com.firebase.ui.auth.IdpResponse; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Result of launching a {@link FirebaseAuthUIActivityResultContract} - */ -public class FirebaseAuthUIAuthenticationResult { - - @Nullable - private final IdpResponse idpResponse; - @NonNull - private final Integer resultCode; - - public FirebaseAuthUIAuthenticationResult(@NonNull Integer resultCode, @Nullable IdpResponse idpResponse) { - this.idpResponse = idpResponse; - this.resultCode = resultCode; - } - - /** - * The contained {@link IdpResponse} returned from the Firebase library - */ - @Nullable - public IdpResponse getIdpResponse() { - return idpResponse; - } - - /** - * The result code of the received activity result - * - * @see android.app.Activity.RESULT_CANCELED - * @see android.app.Activity.RESULT_OK - */ - @NonNull - public Integer getResultCode() { - return resultCode; - } - - @Override - public int hashCode() { - int result = idpResponse == null ? 0 : idpResponse.hashCode(); - result = 31 * result + resultCode.hashCode(); - return result; - } - - @Override - public String toString() { - return "FirebaseAuthUIAuthenticationResult{" + - "idpResponse=" + idpResponse + - ", resultCode='" + resultCode + - '}'; - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/data/model/FirebaseAuthUIAuthenticationResult.kt b/auth/src/main/java/com/firebase/ui/auth/data/model/FirebaseAuthUIAuthenticationResult.kt new file mode 100644 index 000000000..4372307e7 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/data/model/FirebaseAuthUIAuthenticationResult.kt @@ -0,0 +1,20 @@ +package com.firebase.ui.auth.data.model + +import com.firebase.ui.auth.IdpResponse + +/** + * Result of launching a [FirebaseAuthUIActivityResultContract] + */ +data class FirebaseAuthUIAuthenticationResult( + /** + * The result code of the received activity result + * + * @see android.app.Activity.RESULT_CANCELED + * @see android.app.Activity.RESULT_OK + */ + val resultCode: Int, + /** + * The contained [IdpResponse] returned from the Firebase library + */ + val idpResponse: IdpResponse? +) \ No newline at end of file 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 deleted file mode 100644 index c6dac950a..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.java +++ /dev/null @@ -1,223 +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.data.model; - -import android.content.Intent; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; - -import com.firebase.ui.auth.AuthMethodPickerLayout; -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.AuthUI.IdpConfig; -import com.firebase.ui.auth.util.ExtraConstants; -import com.firebase.ui.auth.util.Preconditions; -import com.google.firebase.auth.ActionCodeSettings; -import com.google.firebase.auth.GoogleAuthProvider; - -import java.util.Collections; -import java.util.List; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.StyleRes; - -/** - * Encapsulates the core parameters and data captured during the authentication flow, in a - * serializable manner, in order to pass data between activities. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class FlowParameters implements Parcelable { - - public static final Creator CREATOR = new Creator() { - @Override - public FlowParameters createFromParcel(Parcel in) { - String appName = in.readString(); - List providerInfo = in.createTypedArrayList(IdpConfig.CREATOR); - IdpConfig defaultProvider = in.readParcelable(IdpConfig.class.getClassLoader()); - int themeId = in.readInt(); - int logoId = in.readInt(); - String termsOfServiceUrl = in.readString(); - String privacyPolicyUrl = in.readString(); - boolean enableCredentials = in.readInt() != 0; - boolean enableHints = in.readInt() != 0; - boolean enableAnonymousUpgrade = in.readInt() != 0; - boolean alwaysShowProviderChoice = in.readInt() != 0; - boolean lockOrientation = in.readInt() != 0; - String emailLink = in.readString(); - ActionCodeSettings passwordResetSettings = in.readParcelable(ActionCodeSettings.class.getClassLoader()); - AuthMethodPickerLayout customLayout = in.readParcelable(AuthMethodPickerLayout.class.getClassLoader()); - - return new FlowParameters( - appName, - providerInfo, - defaultProvider, - themeId, - logoId, - termsOfServiceUrl, - privacyPolicyUrl, - enableCredentials, - enableAnonymousUpgrade, - alwaysShowProviderChoice, - lockOrientation, - emailLink, - passwordResetSettings, - customLayout); - } - - @Override - public FlowParameters[] newArray(int size) { - return new FlowParameters[size]; - } - }; - - @NonNull - public final String appName; - - @NonNull - public final List providers; - - @Nullable - public final IdpConfig defaultProvider; - - @StyleRes - public final int themeId; - - @DrawableRes - public final int logoId; - - @Nullable - public final String termsOfServiceUrl; - - @Nullable - public final String privacyPolicyUrl; - - @Nullable - public String emailLink; - - @Nullable - public final ActionCodeSettings passwordResetSettings; - - public final boolean enableCredentials; - public final boolean enableAnonymousUpgrade; - public final boolean alwaysShowProviderChoice; - public final boolean lockOrientation; - - @Nullable - public final AuthMethodPickerLayout authMethodPickerLayout; - - public FlowParameters( - @NonNull String appName, - @NonNull List providers, - @Nullable IdpConfig defaultProvider, - @StyleRes int themeId, - @DrawableRes int logoId, - @Nullable String termsOfServiceUrl, - @Nullable String privacyPolicyUrl, - boolean enableCredentials, - boolean enableAnonymousUpgrade, - boolean alwaysShowProviderChoice, - boolean lockOrientation, - @Nullable String emailLink, - @Nullable ActionCodeSettings passwordResetSettings, - @Nullable AuthMethodPickerLayout authMethodPickerLayout) { - this.appName = Preconditions.checkNotNull(appName, "appName cannot be null"); - this.providers = Collections.unmodifiableList( - Preconditions.checkNotNull(providers, "providers cannot be null")); - this.defaultProvider = defaultProvider; - this.themeId = themeId; - this.logoId = logoId; - this.termsOfServiceUrl = termsOfServiceUrl; - this.privacyPolicyUrl = privacyPolicyUrl; - this.enableCredentials = enableCredentials; - this.enableAnonymousUpgrade = enableAnonymousUpgrade; - this.alwaysShowProviderChoice = alwaysShowProviderChoice; - this.lockOrientation = lockOrientation; - this.emailLink = emailLink; - this.passwordResetSettings = passwordResetSettings; - this.authMethodPickerLayout = authMethodPickerLayout; - } - - /** - * Extract FlowParameters from an Intent. - */ - public static FlowParameters fromIntent(Intent intent) { - return intent.getParcelableExtra(ExtraConstants.FLOW_PARAMS); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(appName); - dest.writeTypedList(providers); - dest.writeParcelable(defaultProvider, flags); - dest.writeInt(themeId); - dest.writeInt(logoId); - dest.writeString(termsOfServiceUrl); - dest.writeString(privacyPolicyUrl); - dest.writeInt(enableCredentials ? 1 : 0); - dest.writeInt(enableAnonymousUpgrade ? 1 : 0); - dest.writeInt(alwaysShowProviderChoice ? 1 : 0); - dest.writeInt(lockOrientation ? 1 : 0); - dest.writeString(emailLink); - dest.writeParcelable(passwordResetSettings, flags); - dest.writeParcelable(authMethodPickerLayout, flags); - } - - @Override - public int describeContents() { - return 0; - } - - public boolean isSingleProviderFlow() { - return providers.size() == 1; - } - - public boolean isTermsOfServiceUrlProvided() { - return !TextUtils.isEmpty(termsOfServiceUrl); - } - - public boolean isPrivacyPolicyUrlProvided() { - return !TextUtils.isEmpty(privacyPolicyUrl); - } - - public boolean isAnonymousUpgradeEnabled() { - return enableAnonymousUpgrade; - } - - public boolean isPlayServicesRequired() { - // Play services only required for Google Sign In and the Credentials API - return isProviderEnabled(GoogleAuthProvider.PROVIDER_ID) - || enableCredentials; - } - - public boolean isProviderEnabled(@AuthUI.SupportedProvider String provider) { - for (AuthUI.IdpConfig idp : providers) { - if (idp.getProviderId().equals(provider)) { - return true; - } - } - - return false; - } - - public boolean shouldShowProviderChoice() { - return defaultProvider == null && (!isSingleProviderFlow() || alwaysShowProviderChoice); - } - - public IdpConfig getDefaultOrFirstProvider() { - return defaultProvider != null ? defaultProvider : providers.get(0); - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.kt b/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.kt new file mode 100644 index 000000000..8d0a2abd0 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.kt @@ -0,0 +1,152 @@ +/* + * 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.data.model + +import android.content.Intent +import android.os.Parcel +import android.os.Parcelable +import android.text.TextUtils +import com.firebase.ui.auth.AuthMethodPickerLayout +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.AuthUI.IdpConfig +import com.firebase.ui.auth.util.ExtraConstants +import com.firebase.ui.auth.util.Preconditions +import com.google.firebase.auth.ActionCodeSettings +import com.google.firebase.auth.GoogleAuthProvider +import java.util.Collections +import androidx.annotation.DrawableRes +import androidx.annotation.NonNull +import androidx.annotation.Nullable +import androidx.annotation.RestrictTo +import androidx.annotation.StyleRes + +/** + * Encapsulates the core parameters and data captured during the authentication flow, in a + * serializable manner, in order to pass data between activities. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class FlowParameters( + @JvmField val appName: String, + providers: List, + @JvmField val defaultProvider: IdpConfig?, + @StyleRes @JvmField val themeId: Int, + @DrawableRes @JvmField val logoId: Int, + @JvmField val termsOfServiceUrl: String?, + @JvmField val privacyPolicyUrl: String?, + @JvmField val enableCredentials: Boolean, + @JvmField val enableAnonymousUpgrade: Boolean, + @JvmField val alwaysShowProviderChoice: Boolean, + @JvmField val lockOrientation: Boolean, + @JvmField var emailLink: String?, + @JvmField val passwordResetSettings: ActionCodeSettings?, + @JvmField val authMethodPickerLayout: AuthMethodPickerLayout? +) : Parcelable { + + // Wrap the providers list in an unmodifiable list to mimic the original behavior. + @JvmField + val providers: List = + Collections.unmodifiableList(Preconditions.checkNotNull(providers, "providers cannot be null")) + + init { + Preconditions.checkNotNull(appName, "appName cannot be null") + } + + /** + * Constructor used for parcelable. + */ + private constructor(parcel: Parcel) : this( + appName = Preconditions.checkNotNull(parcel.readString(), "appName cannot be null"), + providers = parcel.createTypedArrayList(IdpConfig.CREATOR) + ?: emptyList(), + defaultProvider = parcel.readParcelable(IdpConfig::class.java.classLoader), + themeId = parcel.readInt(), + logoId = parcel.readInt(), + termsOfServiceUrl = parcel.readString(), + privacyPolicyUrl = parcel.readString(), + enableCredentials = parcel.readInt() != 0, + enableAnonymousUpgrade = parcel.readInt() != 0, + alwaysShowProviderChoice = parcel.readInt() != 0, + lockOrientation = parcel.readInt() != 0, + emailLink = parcel.readString(), + passwordResetSettings = parcel.readParcelable(ActionCodeSettings::class.java.classLoader), + authMethodPickerLayout = parcel.readParcelable(AuthMethodPickerLayout::class.java.classLoader) + ) + + /** + * Extract FlowParameters from an Intent. + */ + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): FlowParameters { + return FlowParameters(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + @JvmStatic + fun fromIntent(intent: Intent): FlowParameters = + // getParcelableExtra returns a nullable type so we use !! to mirror the Java behavior. + intent.getParcelableExtra(ExtraConstants.FLOW_PARAMS)!! + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(appName) + dest.writeTypedList(providers) + dest.writeParcelable(defaultProvider, flags) + dest.writeInt(themeId) + dest.writeInt(logoId) + dest.writeString(termsOfServiceUrl) + dest.writeString(privacyPolicyUrl) + dest.writeInt(if (enableCredentials) 1 else 0) + dest.writeInt(if (enableAnonymousUpgrade) 1 else 0) + dest.writeInt(if (alwaysShowProviderChoice) 1 else 0) + dest.writeInt(if (lockOrientation) 1 else 0) + dest.writeString(emailLink) + dest.writeParcelable(passwordResetSettings, flags) + dest.writeParcelable(authMethodPickerLayout, flags) + } + + override fun describeContents(): Int = 0 + + fun isSingleProviderFlow(): Boolean = providers.size == 1 + + fun isTermsOfServiceUrlProvided(): Boolean = !TextUtils.isEmpty(termsOfServiceUrl) + + fun isPrivacyPolicyUrlProvided(): Boolean = !TextUtils.isEmpty(privacyPolicyUrl) + + fun isAnonymousUpgradeEnabled(): Boolean = enableAnonymousUpgrade + + fun isPlayServicesRequired(): Boolean { + // Play services only required for Google Sign In and the Credentials API + return isProviderEnabled(GoogleAuthProvider.PROVIDER_ID) || enableCredentials + } + + fun isProviderEnabled(@AuthUI.SupportedProvider provider: String): Boolean { + for (idp in providers) { + if (idp.providerId == provider) { + return true + } + } + return false + } + + fun shouldShowProviderChoice(): Boolean { + return defaultProvider == null && (!isSingleProviderFlow() || alwaysShowProviderChoice) + } + + fun getDefaultOrFirstProvider(): IdpConfig { + return defaultProvider ?: providers[0] + } +} \ No newline at end of file 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 index b665ce1d4..7f9f3ae79 100644 --- 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 @@ -75,7 +75,7 @@ class SignInKickstarter(application: Application?) : SignInViewModelBase(applica */ private fun startAuthMethodChoice() { if (!arguments.shouldShowProviderChoice()) { - val firstIdpConfig = arguments.defaultOrFirstProvider + val firstIdpConfig = arguments.getDefaultOrFirstProvider() val firstProvider = firstIdpConfig.providerId when (firstProvider) { AuthUI.EMAIL_LINK_PROVIDER, EmailAuthProvider.PROVIDER_ID -> @@ -91,7 +91,7 @@ class SignInKickstarter(application: Application?) : SignInViewModelBase(applica setResult( Resource.forFailure( IntentRequiredException( - PhoneActivity.createIntent(app, arguments, firstIdpConfig.params), + PhoneActivity.createIntent(app, arguments, firstIdpConfig.getParams()), RequestCodes.PHONE_FLOW ) ) 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 deleted file mode 100644 index e423a30c7..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.firebase.ui.auth.ui.email; - -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.TextView; - -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; -import com.firebase.ui.auth.ui.FragmentBase; -import com.firebase.ui.auth.util.ExtraConstants; -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.google.android.material.snackbar.Snackbar; -import com.google.android.material.textfield.TextInputLayout; -import com.google.firebase.auth.EmailAuthProvider; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.ViewModelProvider; - -import static com.firebase.ui.auth.AuthUI.EMAIL_LINK_PROVIDER; - -/** - * Fragment that shows a form with an email field and checks for existing accounts with that email. - *

- * Host Activities should implement {@link CheckEmailFragment.CheckEmailListener}. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class CheckEmailFragment extends FragmentBase implements - View.OnClickListener, - ImeHelper.DonePressedListener { - - public static final String TAG = "CheckEmailFragment"; - private CheckEmailHandler mHandler; - private Button mSignInButton; - private Button mSignUpButton; - private ProgressBar mProgressBar; - private EditText mEmailEditText; - private TextInputLayout mEmailLayout; - private EmailFieldValidator mEmailFieldValidator; - private CheckEmailListener mListener; - - public static CheckEmailFragment newInstance(@Nullable String email) { - CheckEmailFragment fragment = new CheckEmailFragment(); - Bundle args = new Bundle(); - args.putString(ExtraConstants.EMAIL, email); - fragment.setArguments(args); - return fragment; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fui_check_email_layout, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - mSignInButton = view.findViewById(R.id.button_sign_in); - mSignUpButton = view.findViewById(R.id.button_sign_up); - mProgressBar = view.findViewById(R.id.top_progress_bar); - - mEmailLayout = view.findViewById(R.id.email_layout); - mEmailEditText = view.findViewById(R.id.email); - mEmailFieldValidator = new EmailFieldValidator(mEmailLayout); - mEmailLayout.setOnClickListener(this); - mEmailEditText.setOnClickListener(this); - - TextView headerText = view.findViewById(R.id.header_text); - if (headerText != null) { - headerText.setVisibility(View.GONE); - } - - ImeHelper.setImeOnDoneListener(mEmailEditText, this); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mEmailEditText.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); - } - - // Set listeners for our new sign‑in and sign‑up buttons. - mSignInButton.setOnClickListener(this); - mSignUpButton.setOnClickListener(this); - - TextView termsText = view.findViewById(R.id.email_tos_and_pp_text); - TextView footerText = view.findViewById(R.id.email_footer_tos_and_pp_text); - FlowParameters flowParameters = getFlowParams(); - - if (!flowParameters.shouldShowProviderChoice()) { - PrivacyDisclosureUtils.setupTermsOfServiceAndPrivacyPolicyText(requireContext(), - flowParameters, - termsText); - } else { - termsText.setVisibility(View.GONE); - PrivacyDisclosureUtils.setupTermsOfServiceFooter(requireContext(), - flowParameters, - footerText); - } - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mHandler = new ViewModelProvider(this).get(CheckEmailHandler.class); - mHandler.init(getFlowParams()); - - FragmentActivity activity = getActivity(); - if (!(activity instanceof CheckEmailListener)) { - throw new IllegalStateException("Activity must implement CheckEmailListener"); - } - mListener = (CheckEmailListener) activity; - - // 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); - // Previously auto-triggering the check is now removed. - } else if (getFlowParams().enableCredentials) { - mHandler.fetchCredential(); - } - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - mHandler.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void onClick(View view) { - int id = view.getId(); - - 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); - } - } - - @Override - public void onDonePressed() { - // 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 signUp() { - String email = mEmailEditText.getText().toString(); - if (mEmailFieldValidator.validate(email)) { - String provider = getEmailProvider(); - User user = new User.Builder(provider, email).build(); - mListener.onNewUser(user); - } - } - - @Override - public void showProgress(int message) { - // Disable both buttons while progress is showing. - mSignInButton.setEnabled(false); - mSignUpButton.setEnabled(false); - mProgressBar.setVisibility(View.VISIBLE); - } - - @Override - public void hideProgress() { - mSignInButton.setEnabled(true); - mSignUpButton.setEnabled(true); - mProgressBar.setVisibility(View.INVISIBLE); - } - - /** - * Interface to be implemented by Activities hosting this Fragment. - */ - interface CheckEmailListener { - - /** - * Email entered belongs to an existing email user (sign‑in flow). - */ - void onExistingEmailUser(User user); - - /** - * Email entered belongs to an existing IDP user. - */ - void onExistingIdpUser(User user); - - /** - * Email entered does not belong to an existing user (sign‑up flow). - */ - void onNewUser(User user); - - /** - * Email entered corresponds to an existing user whose sign in methods we do not support. - */ - void onDeveloperFailure(Exception e); - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.kt b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.kt new file mode 100644 index 000000000..718268e35 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.kt @@ -0,0 +1,188 @@ +package com.firebase.ui.auth.ui.email + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.EditText +import android.widget.ProgressBar +import android.widget.TextView +import com.firebase.ui.auth.R +import com.firebase.ui.auth.data.model.FlowParameters +import com.firebase.ui.auth.data.model.User +import com.firebase.ui.auth.ui.FragmentBase +import com.firebase.ui.auth.util.ExtraConstants +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.google.android.material.textfield.TextInputLayout +import com.google.firebase.auth.EmailAuthProvider +import androidx.annotation.RestrictTo +import androidx.lifecycle.ViewModelProvider + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class CheckEmailFragment : FragmentBase(), View.OnClickListener, ImeHelper.DonePressedListener { + + private lateinit var mHandler: CheckEmailHandler + private lateinit var mListener: CheckEmailListener + private lateinit var mEmailEditText: EditText + private lateinit var mEmailLayout: TextInputLayout + private lateinit var mSignInButton: Button + private lateinit var mSignUpButton: Button + private lateinit var mProgressBar: ProgressBar + private lateinit var mEmailFieldValidator: EmailFieldValidator + + companion object { + const val TAG = "CheckEmailFragment" + + @JvmStatic + fun newInstance(email: String?): CheckEmailFragment { + return CheckEmailFragment().apply { + arguments = Bundle().apply { + putString(ExtraConstants.EMAIL, email) + } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fui_check_email_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mSignInButton = view.findViewById(R.id.button_sign_in) + mSignUpButton = view.findViewById(R.id.button_sign_up) + mProgressBar = view.findViewById(R.id.top_progress_bar) + + mEmailLayout = view.findViewById(R.id.email_layout) + mEmailEditText = view.findViewById(R.id.email) + mEmailFieldValidator = EmailFieldValidator(mEmailLayout) + mEmailLayout.setOnClickListener(this) + mEmailEditText.setOnClickListener(this) + + val headerText: TextView? = view.findViewById(R.id.header_text) + headerText?.visibility = View.GONE + + ImeHelper.setImeOnDoneListener(mEmailEditText, this) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mEmailEditText.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO + } + + mSignInButton.setOnClickListener(this) + mSignUpButton.setOnClickListener(this) + + val termsText: TextView? = view.findViewById(R.id.email_tos_and_pp_text) + val footerText: TextView? = view.findViewById(R.id.email_footer_tos_and_pp_text) + val flowParameters: FlowParameters = getFlowParams() + + if (!flowParameters.shouldShowProviderChoice()) { + PrivacyDisclosureUtils.setupTermsOfServiceAndPrivacyPolicyText( + requireContext(), + flowParameters, + termsText + ) + } else { + termsText?.visibility = View.GONE + PrivacyDisclosureUtils.setupTermsOfServiceFooter( + requireContext(), + flowParameters, + footerText + ) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + mHandler = ViewModelProvider(this).get(CheckEmailHandler::class.java) + mHandler.init(getFlowParams()) + + val activity = activity + if (activity !is CheckEmailListener) { + throw IllegalStateException("Activity must implement CheckEmailListener") + } + mListener = activity + + if (savedInstanceState == null) { + val email = arguments?.getString(ExtraConstants.EMAIL) + if (!TextUtils.isEmpty(email)) { + mEmailEditText.setText(email) + } else if (getFlowParams().enableCredentials) { + mHandler.fetchCredential() + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + mHandler.onActivityResult(requestCode, resultCode, data) + } + + override fun onClick(view: View) { + when (view.id) { + R.id.button_sign_in -> signIn() + R.id.button_sign_up -> signUp() + R.id.email_layout, R.id.email -> mEmailLayout.error = null + } + } + + override fun onDonePressed() { + // When the user hits "done" on the keyboard, default to sign‑in. + signIn() + } + + private fun getEmailProvider(): String { + for (config in getFlowParams().providers) { + if (EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD == config.providerId) { + return EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD + } + } + return EmailAuthProvider.PROVIDER_ID + } + + private fun signIn() { + val email = mEmailEditText.text.toString() + if (mEmailFieldValidator.validate(email)) { + val provider = getEmailProvider() + val user = User.Builder(provider, email).build() + mListener.onExistingEmailUser(user) + } + } + + private fun signUp() { + val email = mEmailEditText.text.toString() + if (mEmailFieldValidator.validate(email)) { + val provider = getEmailProvider() + val user = User.Builder(provider, email).build() + mListener.onNewUser(user) + } + } + + override fun showProgress(message: Int) { + mSignInButton.isEnabled = false + mSignUpButton.isEnabled = false + mProgressBar.visibility = View.VISIBLE + } + + override fun hideProgress() { + mSignInButton.isEnabled = true + mSignUpButton.isEnabled = true + mProgressBar.visibility = View.INVISIBLE + } + + interface CheckEmailListener { + fun onExistingEmailUser(user: User) + fun onExistingIdpUser(user: User) + fun onNewUser(user: User) + fun onDeveloperFailure(e: Exception) + } +} \ No newline at end of file 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 deleted file mode 100644 index 3a911a417..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.firebase.ui.auth.ui.email; - -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; -import com.firebase.ui.auth.data.model.User; -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.common.api.ApiException; -import com.google.android.gms.tasks.Task; - -// 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() { - // 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) - .addOnCompleteListener(task -> { - if (task.isSuccessful()) { - setResult(Resource.forSuccess( - new User.Builder(task.getResult(), email).build())); - } else { - setResult(Resource.forFailure(task.getException())); - } - }); - } - - /** - * 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; - } - - setResult(Resource.forLoading()); - 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/CheckEmailHandler.kt b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.kt new file mode 100644 index 000000000..9ba6da0f0 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.kt @@ -0,0 +1,106 @@ +package com.firebase.ui.auth.ui.email + +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 +import com.firebase.ui.auth.data.model.User +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.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.auth.api.identity.SignInClient +import com.google.android.gms.common.api.ApiException +import androidx.annotation.Nullable + +class CheckEmailHandler(application: Application) : AuthViewModelBase(application) { + companion object { + private const val TAG = "CheckEmailHandler" + } + + /** + * Initiates a hint picker flow using the new Identity API. + * This replaces the deprecated Credentials API call. + */ + fun fetchCredential() { + val signInClient: SignInClient = Identity.getSignInClient(getApplication()) + val 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. + val pendingIntent: PendingIntent = result.pendingIntent + setResult( + Resource.forFailure( + 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. + */ + fun fetchProvider(email: String) { + setResult(Resource.forLoading()) + ProviderUtils.fetchTopProvider(getAuth(), getArguments(), email) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + setResult(Resource.forSuccess(User.Builder(task.result, email).build())) + } else { + setResult(Resource.forFailure(task.exception ?: Exception("Unknown error"))) + } + } + } + + /** + * Handles the result from the hint picker launched via the new Identity API. + */ + fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) { + if (requestCode != RequestCodes.CRED_HINT || resultCode != Activity.RESULT_OK) { + return + } + + setResult(Resource.forLoading()) + val signInClient: SignInClient = Identity.getSignInClient(getApplication()) + try { + // Retrieve the SignInCredential from the returned intent. + val credential: SignInCredential = signInClient.getSignInCredentialFromIntent(data) + val email: String = credential.id + + ProviderUtils.fetchTopProvider(getAuth(), getArguments(), email) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + setResult( + Resource.forSuccess( + User.Builder(task.result, email) + .setName(credential.displayName) + .setPhotoUri(credential.profilePictureUri) + .build() + ) + ) + } else { + setResult(Resource.forFailure(task.exception ?: Exception("Unknown error"))) + } + } + } catch (e: ApiException) { + Log.e(TAG, "getSignInCredentialFromIntent failed", e) + setResult(Resource.forFailure(e)) + } + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.java deleted file mode 100644 index feabf506d..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.java +++ /dev/null @@ -1,247 +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.email; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.ErrorCodes; -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.User; -import com.firebase.ui.auth.ui.AppCompatBase; -import com.firebase.ui.auth.ui.idp.WelcomeBackIdpPrompt; -import com.firebase.ui.auth.util.ExtraConstants; -import com.firebase.ui.auth.util.data.EmailLinkPersistenceManager; -import com.firebase.ui.auth.util.data.ProviderUtils; -import com.firebase.ui.auth.viewmodel.RequestCodes; -import com.google.android.material.textfield.TextInputLayout; -import com.google.firebase.auth.ActionCodeSettings; -import com.google.firebase.auth.EmailAuthProvider; - -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.StringRes; -import androidx.core.view.ViewCompat; -import androidx.fragment.app.FragmentTransaction; - -import static com.firebase.ui.auth.AuthUI.EMAIL_LINK_PROVIDER; - -/** - * Activity to control the entire email sign up flow. Plays host to {@link CheckEmailFragment} and - * {@link RegisterEmailFragment} and triggers {@link WelcomeBackPasswordPrompt} and {@link - * WelcomeBackIdpPrompt}. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class EmailActivity extends AppCompatBase implements CheckEmailFragment.CheckEmailListener, - RegisterEmailFragment.AnonymousUpgradeListener, EmailLinkFragment - .TroubleSigningInListener, TroubleSigningInFragment.ResendEmailListener { - - public static Intent createIntent(Context context, FlowParameters flowParams) { - return createBaseIntent(context, EmailActivity.class, flowParams); - } - - public static Intent createIntent(Context context, FlowParameters flowParams, String email) { - return createBaseIntent(context, EmailActivity.class, flowParams) - .putExtra(ExtraConstants.EMAIL, email); - } - - public static Intent createIntentForLinking(Context context, FlowParameters flowParams, - IdpResponse responseForLinking) { - return createIntent(context, flowParams, responseForLinking.getEmail()) - .putExtra(ExtraConstants.IDP_RESPONSE, responseForLinking); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.fui_activity_register_email); - - if (savedInstanceState != null) { - return; - } - - // Get email from intent (can be null) - String email = getIntent().getExtras().getString(ExtraConstants.EMAIL); - - IdpResponse responseForLinking = getIntent().getExtras().getParcelable(ExtraConstants - .IDP_RESPONSE); - if (email != null && responseForLinking != null) { - // got here from WelcomeBackEmailLinkPrompt - AuthUI.IdpConfig emailConfig = ProviderUtils.getConfigFromIdpsOrThrow( - getFlowParams().providers, EMAIL_LINK_PROVIDER); - ActionCodeSettings actionCodeSettings = emailConfig.getParams().getParcelable - (ExtraConstants.ACTION_CODE_SETTINGS); - - EmailLinkPersistenceManager.getInstance().saveIdpResponseForLinking(getApplication(), - responseForLinking); - - boolean forceSameDevice = - emailConfig.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE); - EmailLinkFragment fragment = EmailLinkFragment.newInstance(email, actionCodeSettings, - responseForLinking, forceSameDevice); - switchFragment(fragment, R.id.fragment_register_email, EmailLinkFragment.TAG); - } else { - AuthUI.IdpConfig emailConfig = ProviderUtils.getConfigFromIdps( - getFlowParams().providers, EmailAuthProvider.PROVIDER_ID); - - if (emailConfig != null) { - email = emailConfig.getParams().getString(ExtraConstants.DEFAULT_EMAIL);; - } - // Start with check email - CheckEmailFragment fragment = CheckEmailFragment.newInstance(email); - switchFragment(fragment, R.id.fragment_register_email, CheckEmailFragment.TAG); - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == RequestCodes.WELCOME_BACK_EMAIL_FLOW - || requestCode == RequestCodes.WELCOME_BACK_IDP_FLOW) { - finish(resultCode, data); - } - } - - @Override - public void onExistingEmailUser(User user) { - if (user.getProviderId().equals(EMAIL_LINK_PROVIDER)) { - AuthUI.IdpConfig emailConfig = ProviderUtils.getConfigFromIdpsOrThrow( - getFlowParams().providers, EMAIL_LINK_PROVIDER); - showRegisterEmailLinkFragment( - emailConfig, user.getEmail()); - } else { - startActivityForResult( - WelcomeBackPasswordPrompt.createIntent( - this, getFlowParams(), new IdpResponse.Builder(user).build()), - RequestCodes.WELCOME_BACK_EMAIL_FLOW); - setSlideAnimation(); - } - } - - @Override - public void onExistingIdpUser(User user) { - // Existing social user, direct them to sign in using their chosen provider. - startActivityForResult( - WelcomeBackIdpPrompt.createIntent(this, getFlowParams(), user), - RequestCodes.WELCOME_BACK_IDP_FLOW); - setSlideAnimation(); - } - - @Override - public void onNewUser(User user) { - // New user, direct them to create an account with email/password - // if account creation is enabled in SignInIntentBuilder - - TextInputLayout emailLayout = findViewById(R.id.email_layout); - AuthUI.IdpConfig emailConfig = ProviderUtils.getConfigFromIdps(getFlowParams().providers, - EmailAuthProvider.PROVIDER_ID); - - if (emailConfig == null) { - emailConfig = ProviderUtils.getConfigFromIdps(getFlowParams().providers, - EMAIL_LINK_PROVIDER); - } - - if (emailConfig.getParams().getBoolean(ExtraConstants.ALLOW_NEW_EMAILS, true)) { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - if (emailConfig.getProviderId().equals(EMAIL_LINK_PROVIDER)) { - showRegisterEmailLinkFragment(emailConfig, user.getEmail()); - } else { - RegisterEmailFragment fragment = RegisterEmailFragment.newInstance(user); - ft.replace(R.id.fragment_register_email, fragment, RegisterEmailFragment.TAG); - if (emailLayout != null) { - String emailFieldName = getString(R.string.fui_email_field_name); - ViewCompat.setTransitionName(emailLayout, emailFieldName); - ft.addSharedElement(emailLayout, emailFieldName); - } - ft.disallowAddToBackStack().commit(); - } - } else { - emailLayout.setError(getString(R.string.fui_error_email_does_not_exist)); - } - } - - @Override - public void onTroubleSigningIn(String email) { - TroubleSigningInFragment troubleSigningInFragment = TroubleSigningInFragment.newInstance - (email); - switchFragment(troubleSigningInFragment, R.id.fragment_register_email, - TroubleSigningInFragment.TAG, true, true); - } - - @Override - public void onClickResendEmail(String email) { - if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - // We're assuming that to get to the TroubleSigningInFragment, we went through - // the EmailLinkFragment, which was added to the fragment back stack. - // From here, we're going to register the EmailLinkFragment again, meaning we'd have to - // pop off the back stack twice to return to the nascar screen. To avoid this, - // we pre-emptively pop off the last EmailLinkFragment here. - getSupportFragmentManager().popBackStack(); - } - AuthUI.IdpConfig emailConfig = ProviderUtils.getConfigFromIdpsOrThrow( - getFlowParams().providers, EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD); - showRegisterEmailLinkFragment( - emailConfig, email); - } - - @Override - public void onSendEmailFailure(Exception e) { - finishOnDeveloperError(e); - } - - @Override - public void onDeveloperFailure(Exception e) { - finishOnDeveloperError(e); - } - - private void finishOnDeveloperError(Exception e) { - finish(RESULT_CANCELED, IdpResponse.getErrorIntent(new FirebaseUiException( - ErrorCodes.DEVELOPER_ERROR, e.getMessage()))); - } - - private void setSlideAnimation() { - // Make the next activity slide in - overridePendingTransition(R.anim.fui_slide_in_right, R.anim.fui_slide_out_left); - } - - private void showRegisterEmailLinkFragment(AuthUI.IdpConfig emailConfig, - String email) { - ActionCodeSettings actionCodeSettings = emailConfig.getParams().getParcelable - (ExtraConstants.ACTION_CODE_SETTINGS); - EmailLinkFragment fragment = EmailLinkFragment.newInstance(email, - actionCodeSettings); - switchFragment(fragment, R.id.fragment_register_email, EmailLinkFragment.TAG); - } - - @Override - public void showProgress(@StringRes int message) { - throw new UnsupportedOperationException("Email fragments must handle progress updates."); - } - - @Override - public void hideProgress() { - throw new UnsupportedOperationException("Email fragments must handle progress updates."); - } - - @Override - public void onMergeFailure(IdpResponse response) { - finish(ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT, response.toIntent()); - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.kt b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.kt new file mode 100644 index 000000000..08b4d979c --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.kt @@ -0,0 +1,296 @@ +/* + * 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.email + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.annotation.Nullable +import androidx.annotation.RestrictTo +import androidx.annotation.StringRes +import androidx.core.view.ViewCompat +import androidx.fragment.app.FragmentTransaction +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.ErrorCodes +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.User +import com.firebase.ui.auth.ui.AppCompatBase +import com.firebase.ui.auth.ui.idp.WelcomeBackIdpPrompt +import com.firebase.ui.auth.util.ExtraConstants +import com.firebase.ui.auth.util.data.EmailLinkPersistenceManager +import com.firebase.ui.auth.util.data.ProviderUtils +import com.firebase.ui.auth.viewmodel.RequestCodes +import com.google.android.material.textfield.TextInputLayout +import com.google.firebase.auth.ActionCodeSettings +import com.google.firebase.auth.EmailAuthProvider + +import com.firebase.ui.auth.ui.email.CheckEmailFragment +import com.firebase.ui.auth.ui.email.RegisterEmailFragment +import com.firebase.ui.auth.ui.email.EmailLinkFragment +import com.firebase.ui.auth.ui.email.TroubleSigningInFragment +import com.firebase.ui.auth.ui.email.WelcomeBackPasswordPrompt + +/** + * Activity to control the entire email sign up flow. Plays host to {@link CheckEmailFragment} and + * {@link RegisterEmailFragment} and triggers {@link WelcomeBackPasswordPrompt} and {@link + * WelcomeBackIdpPrompt}. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class EmailActivity : AppCompatBase(), + CheckEmailFragment.CheckEmailListener, + RegisterEmailFragment.AnonymousUpgradeListener, + EmailLinkFragment.TroubleSigningInListener, + TroubleSigningInFragment.ResendEmailListener { + + private var emailLayout: TextInputLayout? = null + + companion object { + @JvmStatic + fun createIntent(context: Context, flowParams: FlowParameters): Intent { + return createBaseIntent(context, EmailActivity::class.java, flowParams) + } + + @JvmStatic + fun createIntent(context: Context, flowParams: FlowParameters, email: String?): Intent { + return createBaseIntent(context, EmailActivity::class.java, flowParams) + .putExtra(ExtraConstants.EMAIL, email) + } + + @JvmStatic + fun createIntentForLinking( + context: Context, + flowParams: FlowParameters, + responseForLinking: IdpResponse + ): Intent { + return createIntent(context, flowParams, responseForLinking.email) + .putExtra(ExtraConstants.IDP_RESPONSE, responseForLinking) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.fui_activity_register_email) + + emailLayout = findViewById(R.id.email_layout) + + if (savedInstanceState != null) { + return + } + + // Get email from intent (can be null) + var email: String? = intent.extras?.getString(ExtraConstants.EMAIL) + val responseForLinking: IdpResponse? = intent.extras?.getParcelable(ExtraConstants.IDP_RESPONSE) + val user: User? = intent.extras?.getParcelable(ExtraConstants.USER) + if (email != null && responseForLinking != null) { + // Got here from WelcomeBackEmailLinkPrompt. + val emailConfig: AuthUI.IdpConfig = ProviderUtils.getConfigFromIdpsOrThrow( + getFlowParams().providers, + AuthUI.EMAIL_LINK_PROVIDER + ) + val actionCodeSettings: ActionCodeSettings? = + emailConfig.getParams().getParcelable(ExtraConstants.ACTION_CODE_SETTINGS) + if (actionCodeSettings == null) { + finishOnDeveloperError(IllegalStateException("ActionCodeSettings cannot be null for email link sign in.")) + return + } + EmailLinkPersistenceManager.getInstance().saveIdpResponseForLinking(application, responseForLinking) + val forceSameDevice: Boolean = emailConfig.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE) + val fragment = EmailLinkFragment.newInstance(email, actionCodeSettings, responseForLinking, forceSameDevice) + switchFragment(fragment, R.id.fragment_register_email, EmailLinkFragment.TAG) + } else { + var emailConfig: AuthUI.IdpConfig? = ProviderUtils.getConfigFromIdps(getFlowParams().providers, EmailAuthProvider.PROVIDER_ID) + if (emailConfig == null) { + emailConfig = ProviderUtils.getConfigFromIdps(getFlowParams().providers, AuthUI.EMAIL_LINK_PROVIDER) + } + if (emailConfig == null) { + finishOnDeveloperError(IllegalStateException("No email provider configured.")) + return + } + if (emailConfig.getParams().getBoolean(ExtraConstants.ALLOW_NEW_EMAILS, true)) { + val ft: FragmentTransaction = supportFragmentManager.beginTransaction() + if (emailConfig.providerId == AuthUI.EMAIL_LINK_PROVIDER) { + if (email == null) { + finishOnDeveloperError(IllegalStateException("Email cannot be null for email link sign in.")) + return + } + showRegisterEmailLinkFragment(emailConfig, email) + } else { + if (user == null) { + // Use default email from configuration if none was provided via the intent. + if (email == null) { + email = emailConfig.getParams().getString(ExtraConstants.DEFAULT_EMAIL) + } + // Pass the email (which may be null if no default is configured) to the fragment. + val fragment = CheckEmailFragment.newInstance(email) + ft.replace(R.id.fragment_register_email, fragment, CheckEmailFragment.TAG) + emailLayout?.let { + val emailFieldName = getString(R.string.fui_email_field_name) + ViewCompat.setTransitionName(it, emailFieldName) + ft.addSharedElement(it, emailFieldName) + } + ft.disallowAddToBackStack().commit() + return + } + val fragment = RegisterEmailFragment.newInstance(user) + ft.replace(R.id.fragment_register_email, fragment, RegisterEmailFragment.TAG) + emailLayout?.let { + val emailFieldName = getString(R.string.fui_email_field_name) + ViewCompat.setTransitionName(it, emailFieldName) + ft.addSharedElement(it, emailFieldName) + } + ft.disallowAddToBackStack().commit() + } + } else { + emailLayout?.error = getString(R.string.fui_error_email_does_not_exist) + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RequestCodes.WELCOME_BACK_EMAIL_FLOW || + requestCode == RequestCodes.WELCOME_BACK_IDP_FLOW + ) { + finish(resultCode, data) + } + } + + override fun onExistingEmailUser(user: User) { + if (user.providerId == AuthUI.EMAIL_LINK_PROVIDER) { + val emailConfig: AuthUI.IdpConfig = ProviderUtils.getConfigFromIdpsOrThrow( + getFlowParams().providers, + AuthUI.EMAIL_LINK_PROVIDER + ) + val email = user.email + if (email == null) { + finishOnDeveloperError(IllegalStateException("Email cannot be null for email link sign in.")) + return + } + showRegisterEmailLinkFragment(emailConfig, email) + } else { + startActivityForResult( + WelcomeBackPasswordPrompt.createIntent(this, getFlowParams(), IdpResponse.Builder(user).build()), + RequestCodes.WELCOME_BACK_EMAIL_FLOW + ) + setSlideAnimation() + } + } + + override fun onExistingIdpUser(user: User) { + // Existing social user: direct them to sign in using their chosen provider. + startActivityForResult( + WelcomeBackIdpPrompt.createIntent(this, getFlowParams(), user), + RequestCodes.WELCOME_BACK_IDP_FLOW + ) + setSlideAnimation() + } + + override fun onNewUser(user: User) { + // New user: direct them to create an account with email/password if account creation is enabled. + var emailConfig: AuthUI.IdpConfig? = ProviderUtils.getConfigFromIdps(getFlowParams().providers, EmailAuthProvider.PROVIDER_ID) + if (emailConfig == null) { + emailConfig = ProviderUtils.getConfigFromIdps(getFlowParams().providers, AuthUI.EMAIL_LINK_PROVIDER) + } + if (emailConfig == null) { + finishOnDeveloperError(IllegalStateException("No email provider configured.")) + return + } + if (emailConfig.getParams().getBoolean(ExtraConstants.ALLOW_NEW_EMAILS, true)) { + val ft: FragmentTransaction = supportFragmentManager.beginTransaction() + if (emailConfig.providerId == AuthUI.EMAIL_LINK_PROVIDER) { + val email = user.email + if (email == null) { + finishOnDeveloperError(IllegalStateException("Email cannot be null for email link sign in.")) + return + } + showRegisterEmailLinkFragment(emailConfig, email) + } else { + val fragment = RegisterEmailFragment.newInstance(user) + ft.replace(R.id.fragment_register_email, fragment, RegisterEmailFragment.TAG) + emailLayout?.let { + val emailFieldName = getString(R.string.fui_email_field_name) + ViewCompat.setTransitionName(it, emailFieldName) + ft.addSharedElement(it, emailFieldName) + } + ft.disallowAddToBackStack().commit() + } + } else { + emailLayout?.error = getString(R.string.fui_error_email_does_not_exist) + } + } + + override fun onTroubleSigningIn(email: String) { + val troubleSigningInFragment = TroubleSigningInFragment.newInstance(email) + switchFragment(troubleSigningInFragment, R.id.fragment_register_email, TroubleSigningInFragment.TAG, true, true) + } + + override fun onClickResendEmail(email: String) { + if (supportFragmentManager.backStackEntryCount > 0) { + // We assume that to get to TroubleSigningInFragment we went through EmailLinkFragment, + // which was added to the fragment back stack. To avoid needing to pop the back stack twice, + // we preemptively pop off the last EmailLinkFragment. + supportFragmentManager.popBackStack() + } + val emailConfig: AuthUI.IdpConfig = ProviderUtils.getConfigFromIdpsOrThrow( + getFlowParams().providers, + EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD + ) + showRegisterEmailLinkFragment(emailConfig, email) + } + + override fun onSendEmailFailure(e: Exception) { + finishOnDeveloperError(e) + } + + override fun onDeveloperFailure(e: Exception) { + finishOnDeveloperError(e) + } + + private fun finishOnDeveloperError(e: Exception) { + finish( + RESULT_CANCELED, + IdpResponse.getErrorIntent(FirebaseUiException(ErrorCodes.DEVELOPER_ERROR, e.message ?: "Unknown error")) + ) + } + + private fun setSlideAnimation() { + // Make the next activity slide in. + overridePendingTransition(R.anim.fui_slide_in_right, R.anim.fui_slide_out_left) + } + + private fun showRegisterEmailLinkFragment(emailConfig: AuthUI.IdpConfig, email: String) { + val actionCodeSettings: ActionCodeSettings? = emailConfig.getParams().getParcelable(ExtraConstants.ACTION_CODE_SETTINGS) + if (actionCodeSettings == null) { + finishOnDeveloperError(IllegalStateException("ActionCodeSettings cannot be null for email link sign in.")) + return + } + val fragment = EmailLinkFragment.newInstance(email, actionCodeSettings) + switchFragment(fragment, R.id.fragment_register_email, EmailLinkFragment.TAG) + } + + override fun showProgress(@StringRes message: Int) { + throw UnsupportedOperationException("Email fragments must handle progress updates.") + } + + override fun hideProgress() { + throw UnsupportedOperationException("Email fragments must handle progress updates.") + } + + override fun onMergeFailure(response: IdpResponse) { + finish(ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT, response.toIntent()) + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.java deleted file mode 100644 index 50a7bfb0f..000000000 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.firebase.ui.auth.ui.email; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; - -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.ui.InvisibleActivityBase; -import com.firebase.ui.auth.util.ExtraConstants; -import com.firebase.ui.auth.viewmodel.RequestCodes; -import com.firebase.ui.auth.viewmodel.ResourceObserver; -import com.firebase.ui.auth.viewmodel.email.EmailLinkSignInHandler; -import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.lifecycle.ViewModelProvider; - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class EmailLinkCatcherActivity extends InvisibleActivityBase { - - private EmailLinkSignInHandler mHandler; - - public static Intent createIntent(Context context, FlowParameters flowParams) { - return createBaseIntent(context, EmailLinkCatcherActivity.class, flowParams); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - initHandler(); - - if (getFlowParams().emailLink != null) { - mHandler.startSignIn(); - } - } - - private void initHandler() { - mHandler = new ViewModelProvider(this).get(EmailLinkSignInHandler.class); - mHandler.init(getFlowParams()); - mHandler.getOperation().observe(this, new ResourceObserver(this) { - @Override - protected void onSuccess(@NonNull IdpResponse response) { - finish(RESULT_OK, response.toIntent()); - } - - @Override - protected void onFailure(@NonNull final Exception e) { - if (e instanceof UserCancellationException) { - finish(RESULT_CANCELED, null); - } else if (e instanceof FirebaseAuthAnonymousUpgradeException) { - IdpResponse res = ((FirebaseAuthAnonymousUpgradeException) e).getResponse(); - finish(RESULT_CANCELED, new Intent().putExtra(ExtraConstants.IDP_RESPONSE, res)); - } else if (e instanceof FirebaseUiException) { - int errorCode = ((FirebaseUiException) e).getErrorCode(); - if (errorCode == ErrorCodes.EMAIL_LINK_WRONG_DEVICE_ERROR - || errorCode == ErrorCodes.INVALID_EMAIL_LINK_ERROR - || errorCode == ErrorCodes.EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR) { - buildAlertDialog(errorCode).show(); - } else if (errorCode == ErrorCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR - || errorCode == ErrorCodes.EMAIL_MISMATCH_ERROR) { - startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW); - } else if (errorCode == ErrorCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR) { - startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW); - } - } else if (e instanceof FirebaseAuthInvalidCredentialsException) { - startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW); - } else { - finish(RESULT_CANCELED, IdpResponse.getErrorIntent(e)); - } - } - }); - } - - /** - * @param flow must be one of RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW or - * RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW - */ - private void startErrorRecoveryFlow(int flow) { - if (flow != RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW - && flow != RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW) { - throw new IllegalStateException("Invalid flow param. It must be either " + - "RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW or " + - "RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW"); - } - Intent intent = EmailLinkErrorRecoveryActivity.createIntent(getApplicationContext(), - getFlowParams(), flow); - startActivityForResult(intent, flow); - } - - private AlertDialog buildAlertDialog(final int errorCode) { - AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); - - String titleText; - String messageText; - if (errorCode == ErrorCodes.EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR) { - titleText = getString(R.string.fui_email_link_different_anonymous_user_header); - messageText = getString(R.string.fui_email_link_different_anonymous_user_message); - } else if (errorCode == ErrorCodes.INVALID_EMAIL_LINK_ERROR) { - titleText = getString(R.string.fui_email_link_invalid_link_header); - messageText = getString(R.string.fui_email_link_invalid_link_message); - } else { - // Default value - ErrorCodes.EMAIL_LINK_WRONG_DEVICE_ERROR - titleText = getString(R.string.fui_email_link_wrong_device_header); - messageText = getString(R.string.fui_email_link_wrong_device_message); - } - - return alertDialog.setTitle(titleText) - .setMessage(messageText) - .setPositiveButton(R.string.fui_email_link_dismiss_button, - (dialog, id) -> finish(errorCode, null)) - .create(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW - || requestCode == RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW) { - IdpResponse response = IdpResponse.fromResultIntent(data); - // CheckActionCode is called before starting this flow, so we only get here - // if the sign in link is valid - it can only fail by being cancelled. - if (resultCode == RESULT_OK) { - finish(RESULT_OK, response.toIntent()); - } else { - finish(RESULT_CANCELED, null); - } - } - } -} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.kt b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.kt new file mode 100644 index 000000000..0ada7cd1e --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.kt @@ -0,0 +1,158 @@ +/* + * 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.email + +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.annotation.NonNull +import androidx.annotation.Nullable +import androidx.annotation.RestrictTo +import androidx.lifecycle.ViewModelProvider +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.ui.InvisibleActivityBase +import com.firebase.ui.auth.util.ExtraConstants +import com.firebase.ui.auth.viewmodel.RequestCodes +import com.firebase.ui.auth.viewmodel.ResourceObserver +import com.firebase.ui.auth.viewmodel.email.EmailLinkSignInHandler +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException + +// Assuming EmailLinkErrorRecoveryActivity exists in your project. +import com.firebase.ui.auth.ui.email.EmailLinkErrorRecoveryActivity + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class EmailLinkCatcherActivity : InvisibleActivityBase() { + + private lateinit var mHandler: EmailLinkSignInHandler + + companion object { + @JvmStatic + fun createIntent(context: Context, flowParams: FlowParameters): Intent { + return createBaseIntent(context, EmailLinkCatcherActivity::class.java, flowParams) + } + } + + override fun onCreate(@Nullable savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initHandler() + + if (getFlowParams().emailLink != null) { + mHandler.startSignIn() + } + } + + private fun initHandler() { + mHandler = ViewModelProvider(this).get(EmailLinkSignInHandler::class.java) + mHandler.init(getFlowParams()) + mHandler.operation.observe(this, object : ResourceObserver(this) { + override fun onSuccess(@NonNull response: IdpResponse) { + finish(RESULT_OK, response.toIntent()) + } + + override fun onFailure(@NonNull e: Exception) { + when { + e is UserCancellationException -> finish(RESULT_CANCELED, null) + e is FirebaseAuthAnonymousUpgradeException -> { + val res = e.response + finish(RESULT_CANCELED, Intent().putExtra(ExtraConstants.IDP_RESPONSE, res)) + } + e is FirebaseUiException -> { + val errorCode = e.errorCode + when (errorCode) { + ErrorCodes.EMAIL_LINK_WRONG_DEVICE_ERROR, + ErrorCodes.INVALID_EMAIL_LINK_ERROR, + ErrorCodes.EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR -> + buildAlertDialog(errorCode).show() + ErrorCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR, + ErrorCodes.EMAIL_MISMATCH_ERROR -> + startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW) + ErrorCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR -> + startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW) + else -> finish(RESULT_CANCELED, IdpResponse.getErrorIntent(e)) + } + } + e is FirebaseAuthInvalidCredentialsException -> + startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW) + else -> finish(RESULT_CANCELED, IdpResponse.getErrorIntent(e)) + } + } + }) + } + + /** + * @param flow must be one of RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW or + * RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW + */ + private fun startErrorRecoveryFlow(flow: Int) { + if (flow != RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW && + flow != RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW + ) { + throw IllegalStateException( + "Invalid flow param. It must be either " + + "RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW or " + + "RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW" + ) + } + val intent = EmailLinkErrorRecoveryActivity.createIntent(applicationContext, getFlowParams(), flow) + startActivityForResult(intent, flow) + } + + private fun buildAlertDialog(errorCode: Int): AlertDialog { + val builder = AlertDialog.Builder(this) + val (titleText, messageText) = when (errorCode) { + ErrorCodes.EMAIL_LINK_DIFFERENT_ANONYMOUS_USER_ERROR -> Pair( + getString(R.string.fui_email_link_different_anonymous_user_header), + getString(R.string.fui_email_link_different_anonymous_user_message) + ) + ErrorCodes.INVALID_EMAIL_LINK_ERROR -> Pair( + getString(R.string.fui_email_link_invalid_link_header), + getString(R.string.fui_email_link_invalid_link_message) + ) + else -> Pair( + getString(R.string.fui_email_link_wrong_device_header), + getString(R.string.fui_email_link_wrong_device_message) + ) + } + return builder.setTitle(titleText) + .setMessage(messageText) + .setPositiveButton(R.string.fui_email_link_dismiss_button) { _, _ -> + finish(errorCode, null) + } + .create() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW || + requestCode == RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW + ) { + val response = IdpResponse.fromResultIntent(data) + // CheckActionCode is called before starting this flow, so we only get here + // if the sign in link is valid – it can only fail by being cancelled. + if (resultCode == RESULT_OK) { + finish(RESULT_OK, response?.toIntent()) + } else { + finish(RESULT_CANCELED, null) + } + } + } +} \ No newline at end of file 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 index c2cc8ec32..f9df9d25f 100644 --- 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 @@ -83,13 +83,13 @@ import androidx.credentials.PasswordCredential import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialException -import com.firebase.ui.auth.AuthUI.EMAIL_LINK_PROVIDER -import com.firebase.ui.auth.util.ExtraConstants.GENERIC_OAUTH_BUTTON_ID -import com.firebase.ui.auth.util.ExtraConstants.GENERIC_OAUTH_PROVIDER_ID import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.firebase.auth.GoogleAuthCredential +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.AuthUI.Companion.EMAIL_LINK_PROVIDER @androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) class AuthMethodPickerActivity : AppCompatBase() { @@ -335,8 +335,8 @@ class AuthMethodPickerActivity : AppCompatBase() { 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) + if (!TextUtils.isEmpty(idpConfig.getParams().getString(GENERIC_OAUTH_PROVIDER_ID))) { + idpConfig.getParams().getInt(GENERIC_OAUTH_BUTTON_ID) } else { throw IllegalStateException("Unknown provider: ${idpConfig.providerId}") } @@ -391,7 +391,7 @@ class AuthMethodPickerActivity : AppCompatBase() { AuthUI.ANONYMOUS_PROVIDER -> viewModelProvider.get(AnonymousSignInHandler::class.java).initWith(flowParams) GoogleAuthProvider.PROVIDER_ID -> - if (authUI.isUseEmulator) { + if (authUI.isUseEmulator()) { viewModelProvider.get(GenericIdpSignInHandler::class.java) .initWith(GenericIdpSignInHandler.getGenericGoogleConfig()) } else { @@ -399,14 +399,14 @@ class AuthMethodPickerActivity : AppCompatBase() { .initWith(GoogleSignInHandler.Params(idpConfig)) } FacebookAuthProvider.PROVIDER_ID -> - if (authUI.isUseEmulator) { + 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))) { + if (!TextUtils.isEmpty(idpConfig.getParams().getString(GENERIC_OAUTH_PROVIDER_ID))) { viewModelProvider.get(GenericIdpSignInHandler::class.java).initWith(idpConfig) } else { throw IllegalStateException("Unknown provider: $providerId") @@ -434,7 +434,7 @@ class AuthMethodPickerActivity : AppCompatBase() { 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 + val isSocialResponse = AuthUI.isSocialProvider(providerId) && !authUI.isUseEmulator() if (!response.isSuccessful) { mHandler.startSignIn(response) } else if (isSocialResponse) { diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/idp/SingleSignInActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/idp/SingleSignInActivity.java index 17221f7ed..dec97c33e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/idp/SingleSignInActivity.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/idp/SingleSignInActivity.java @@ -93,7 +93,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mProvider.getOperation().observe(this, new ResourceObserver(this) { @Override protected void onSuccess(@NonNull IdpResponse response) { - boolean useSocialHandler = AuthUI.SOCIAL_PROVIDERS.contains(provider) + boolean useSocialHandler = AuthUI.isSocialProvider(provider) && !getAuthUI().isUseEmulator(); if (useSocialHandler || !response.isSuccessful()) { diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/idp/WelcomeBackIdpPrompt.java b/auth/src/main/java/com/firebase/ui/auth/ui/idp/WelcomeBackIdpPrompt.java index 0f3e2319c..a63b1a459 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/idp/WelcomeBackIdpPrompt.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/idp/WelcomeBackIdpPrompt.java @@ -153,7 +153,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void onSuccess(@NonNull IdpResponse response) { boolean isGenericIdp = getAuthUI().isUseEmulator() - || !AuthUI.SOCIAL_PROVIDERS.contains(response.getProviderType()); + || !AuthUI.isSocialProvider(response.getProviderType()); if (isGenericIdp && !response.hasCredentialForLinking() diff --git a/auth/src/main/java/com/firebase/ui/auth/viewmodel/email/WelcomeBackPasswordHandler.java b/auth/src/main/java/com/firebase/ui/auth/viewmodel/email/WelcomeBackPasswordHandler.java index d68b93b39..75ae30133 100644 --- a/auth/src/main/java/com/firebase/ui/auth/viewmodel/email/WelcomeBackPasswordHandler.java +++ b/auth/src/main/java/com/firebase/ui/auth/viewmodel/email/WelcomeBackPasswordHandler.java @@ -72,7 +72,7 @@ public void startSignIn(@NonNull final String email, final AuthCredential credToValidate = EmailAuthProvider.getCredential(email, password); // Check to see if we need to link (for social providers with the same email) - if (AuthUI.SOCIAL_PROVIDERS.contains(inputResponse.getProviderType())) { + if (AuthUI.isSocialProvider(inputResponse.getProviderType())) { // Add the provider to the same account before triggering a merge failure. authOperationManager.safeLink(credToValidate, credential, getArguments()) .addOnSuccessListener(result -> handleMergeFailure(credToValidate)) diff --git a/auth/src/main/java/com/firebase/ui/auth/viewmodel/idp/LinkingSocialProviderResponseHandler.java b/auth/src/main/java/com/firebase/ui/auth/viewmodel/idp/LinkingSocialProviderResponseHandler.java index 5e5b1def9..b03092b35 100644 --- a/auth/src/main/java/com/firebase/ui/auth/viewmodel/idp/LinkingSocialProviderResponseHandler.java +++ b/auth/src/main/java/com/firebase/ui/auth/viewmodel/idp/LinkingSocialProviderResponseHandler.java @@ -128,7 +128,7 @@ public boolean hasCredentialForLinking() { private boolean isGenericIdpLinkingFlow(@NonNull String providerId) { // TODO(lsirac): Remove use of SUPPORTED_OAUTH_PROVIDERS when we decide to support all IDPs - return AuthUI.SUPPORTED_OAUTH_PROVIDERS.contains(providerId) + return AuthUI.isSupportedOAuthProvider(providerId) && mRequestedSignInCredential != null && getAuth().getCurrentUser() != null && !getAuth().getCurrentUser().isAnonymous();