From a5d463f4601df383836a9225ef71ac1b11c8eada Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 6 Mar 2025 14:59:02 +0100 Subject: [PATCH] feat(auth): migrate to Kotlin --- .../java/com/firebase/ui/auth/AuthUI.java | 1402 ----------------- .../main/java/com/firebase/ui/auth/AuthUI.kt | 1288 +++++++++++++++ .../java/com/firebase/ui/auth/ErrorCodes.java | 144 -- .../java/com/firebase/ui/auth/ErrorCodes.kt | 124 ++ 4 files changed, 1412 insertions(+), 1546 deletions(-) delete mode 100644 auth/src/main/java/com/firebase/ui/auth/AuthUI.java create mode 100644 auth/src/main/java/com/firebase/ui/auth/AuthUI.kt delete mode 100644 auth/src/main/java/com/firebase/ui/auth/ErrorCodes.java create mode 100644 auth/src/main/java/com/firebase/ui/auth/ErrorCodes.kt 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..beb1a54ee --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/AuthUI.kt @@ -0,0 +1,1288 @@ +/* + * 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 androidx.annotation.* +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.* +import com.firebase.ui.auth.util.data.PhoneNumberUtils +import com.firebase.ui.auth.util.data.ProviderAvailability +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.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.* +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.collections.HashSet + +private const val ANONYMOUS_PROVIDER = "anonymous" +private const val EMAIL_LINK_PROVIDER = EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD +private const val MICROSOFT_PROVIDER = "microsoft.com" +private const val YAHOO_PROVIDER = "yahoo.com" +private const val APPLE_PROVIDER = "apple.com" + +/** + * The set of authentication providers supported in Firebase Auth UI. + */ +private val SUPPORTED_PROVIDERS: Set = Collections.unmodifiableSet( + HashSet( + listOf( + 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). + */ +private val SUPPORTED_OAUTH_PROVIDERS: Set = Collections.unmodifiableSet( + HashSet( + listOf( + 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. + */ +private val SOCIAL_PROVIDERS: Set = Collections.unmodifiableSet( + HashSet( + listOf( + GoogleAuthProvider.PROVIDER_ID, + FacebookAuthProvider.PROVIDER_ID + ) + ) +) + +@Retention(AnnotationRetention.SOURCE) +@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 +) +annotation class SupportedProvider + +/** + * Configuration for an identity provider. + */ +class IdpConfig private constructor( + @SupportedProvider private val mProviderId: String, + private val mParams: Bundle +) : Parcelable { + @SupportedProvider + fun getProviderId(): String = mProviderId + + /** + * @return provider-specific options + */ + 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 { + val providerId = parcel.readString()!! + val params = parcel.readBundle(IdpConfig::class.java.classLoader)!! + return IdpConfig(providerId, params) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + /** + * Base builder for all authentication providers. + * + * @see SignInIntentBuilder.setAvailableProviders + */ + open class Builder protected constructor(@SupportedProvider providerId: String) { + protected val mParams = Bundle() + @SupportedProvider + protected var mProviderId: String = providerId + + init { + require(SUPPORTED_PROVIDERS.contains(providerId) || + SUPPORTED_OAUTH_PROVIDERS.contains(providerId)) { + "Unknown provider: $providerId" + } + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected fun getParams(): Bundle = mParams + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected fun setProviderId(providerId: String) { + mProviderId = providerId + } + + open fun build(): IdpConfig = IdpConfig(mProviderId, mParams) + } + + /** + * [IdpConfig] builder for the email provider. + */ + class EmailBuilder : Builder(EmailAuthProvider.PROVIDER_ID) { + /** + * Enables or disables creating new accounts in the email sign in flows. + * + * Account creation is enabled by default. + */ + fun setAllowNewAccounts(allow: Boolean): EmailBuilder { + mParams.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. + */ + fun setRequireName(requireName: Boolean): EmailBuilder { + mParams.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 [ActionCodeSettings] object using [setActionCodeSettings] + * + * You must enable Firebase Dynamic Links in the Firebase Console to use email link + * sign in. + * + * @throws IllegalStateException if [ActionCodeSettings] is null or not + * provided with email link enabled. + */ + fun enableEmailLinkSignIn(): EmailBuilder { + setProviderId(EMAIL_LINK_PROVIDER) + return this + } + + /** + * Sets the [ActionCodeSettings] object to be used for email link sign in. + * + * [ActionCodeSettings.canHandleCodeInApp] must be set to true, and a valid + * continueUrl must be passed via [ActionCodeSettings.Builder.setUrl]. + * This URL must be allowlisted in the Firebase Console. + * + * @throws IllegalStateException if canHandleCodeInApp is set to false + * @throws NullPointerException if ActionCodeSettings is null + */ + fun setActionCodeSettings(actionCodeSettings: ActionCodeSettings): EmailBuilder { + mParams.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. + */ + fun setForceSameDevice(): EmailBuilder { + mParams.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. + */ + fun setDefaultEmail(email: String): EmailBuilder { + mParams.putString(ExtraConstants.DEFAULT_EMAIL, email) + return this + } + + override fun build(): IdpConfig { + if (mProviderId == EMAIL_LINK_PROVIDER) { + val actionCodeSettings: ActionCodeSettings? = + mParams.getParcelable(ExtraConstants.ACTION_CODE_SETTINGS) + requireNotNull(actionCodeSettings) { + "ActionCodeSettings cannot be null when using email link sign in." + } + require(actionCodeSettings.canHandleCodeInApp()) { + "You must set canHandleCodeInApp in your ActionCodeSettings to true for " + + "Email-Link Sign-in." + } + } + return super.build() + } + } + + /** + * [IdpConfig] builder for the phone provider. + */ + class PhoneBuilder : Builder(PhoneAuthProvider.PROVIDER_ID) { + /** + * @param number the phone number in international format + * @see setDefaultNumber + */ + fun setDefaultNumber(number: String): PhoneBuilder { + checkUnset() + require(PhoneNumberUtils.isValid(number)) { "Invalid phone number: $number" } + mParams.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 + */ + fun setDefaultNumber(iso: String, number: String): PhoneBuilder { + checkUnset() + require(PhoneNumberUtils.isValidIso(iso)) { "Invalid country iso: $iso" } + mParams.putString(ExtraConstants.COUNTRY_ISO, iso) + mParams.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 + */ + fun setDefaultCountryIso(iso: String): PhoneBuilder { + checkUnset() + require(PhoneNumberUtils.isValidIso(iso)) { "Invalid country iso: $iso" } + mParams.putString( + ExtraConstants.COUNTRY_ISO, + iso.uppercase(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 [setBlockedCountries]. + * 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. + */ + fun setAllowedCountries(countries: List): PhoneBuilder { + require(!mParams.containsKey(ExtraConstants.BLOCKLISTED_COUNTRIES)) { + "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." + requireNotNull(countries) { String.format(message, "null") } + require(countries.isNotEmpty()) { 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 [setAllowedCountries]. + * 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. + */ + fun setBlockedCountries(countries: List): PhoneBuilder { + require(!mParams.containsKey(ExtraConstants.ALLOWLISTED_COUNTRIES)) { + "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." + requireNotNull(countries) { String.format(message, "null") } + require(countries.isNotEmpty()) { String.format(message, "empty") } + + addCountriesToBundle(countries, ExtraConstants.BLOCKLISTED_COUNTRIES) + return this + } + + override fun build(): IdpConfig { + validateInputs() + return super.build() + } + + private fun checkUnset() { + Preconditions.checkUnset( + mParams, + "Cannot overwrite previously set phone number", + ExtraConstants.PHONE, + ExtraConstants.COUNTRY_ISO, + ExtraConstants.NATIONAL_NUMBER + ) + } + + private fun addCountriesToBundle(countryIsos: List, countryIsoType: String) { + val uppercaseCodes = countryIsos.map { it.uppercase(Locale.getDefault()) } + mParams.putStringArrayList(countryIsoType, ArrayList(uppercaseCodes)) + } + + private fun validateInputs() { + val allowedCountries = mParams.getStringArrayList(ExtraConstants.ALLOWLISTED_COUNTRIES) + val blockedCountries = mParams.getStringArrayList(ExtraConstants.BLOCKLISTED_COUNTRIES) + + when { + allowedCountries != null && blockedCountries != null -> { + throw IllegalStateException( + "You can either allowlist or blocked country codes for phone authentication." + ) + } + allowedCountries != null -> validateInputs(allowedCountries, true) + 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 (mParams.containsKey(ExtraConstants.COUNTRY_ISO) || + mParams.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 upperIso = iso.uppercase(Locale.getDefault()) + for (code in codes) { + if (PhoneNumberUtils.isValidIso(code)) { + if (code == upperIso) { + return true + } + } else { + val isos = PhoneNumberUtils.getCountryIsosFromCountryCode(code) + if (isos?.contains(upperIso) == true) { + return true + } + } + } + return false + } + + private fun getPhoneIsosFromCode(): List { + val isos = mutableListOf() + val phone = mParams.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? = + if (mParams.containsKey(ExtraConstants.COUNTRY_ISO)) { + mParams.getString(ExtraConstants.COUNTRY_ISO) + } else null + } + + /** + * [IdpConfig] builder for the Google provider. + */ + 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 + ) + } + + /** + * Set the scopes that your app will request when using Google sign-in. See all + * [available scopes](https://developers.google.com/identity/protocols/googlescopes). + * + * @param scopes additional scopes to be requested + */ + 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()) + } + + /** + * Set the [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 + */ + fun setSignInOptions(options: GoogleSignInOptions): GoogleBuilder { + Preconditions.checkUnset( + mParams, + "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) + } + + // Warn the user that they are _probably_ doing the wrong thing if they + // have not called requestEmail (see issue #1899 and #1621) + var hasEmailScope = false + for (s in options.scopes) { + if ("email" == s.scopeUri) { + hasEmailScope = true + break + } + } + if (!hasEmailScope) { + Log.w( + AuthUI.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) + mParams.putParcelable(ExtraConstants.GOOGLE_SIGN_IN_OPTIONS, builder.build()) + + return this + } + + override fun build(): IdpConfig { + if (!mParams.containsKey(ExtraConstants.GOOGLE_SIGN_IN_OPTIONS)) { + validateWebClientId() + setScopes(emptyList()) + } + return super.build() + } + } + + /** + * [IdpConfig] builder for the Facebook provider. + */ + class FacebookBuilder : Builder(FacebookAuthProvider.PROVIDER_ID) { + companion object { + private const val TAG = "FacebookBuilder" + } + + 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.") + } + } + + /** + * Specifies the additional permissions that the application will request in the + * Facebook Login SDK. Available permissions can be found + * [here](https://developers.facebook.com/docs/facebook-login/permissions). + */ + fun setPermissions(permissions: List): FacebookBuilder { + mParams.putStringArrayList( + ExtraConstants.FACEBOOK_PERMISSIONS, + ArrayList(permissions) + ) + return this + } + } + + /** + * [IdpConfig] builder for the Anonymous provider. + */ + class AnonymousBuilder : Builder(ANONYMOUS_PROVIDER) + + /** + * [IdpConfig] builder for the Twitter provider. + */ + class TwitterBuilder : GenericOAuthProviderBuilder( + TwitterAuthProvider.PROVIDER_ID, + "Twitter", + R.layout.fui_idp_button_twitter + ) + + /** + * [IdpConfig] builder for the GitHub provider. + */ + class GitHubBuilder : GenericOAuthProviderBuilder( + GithubAuthProvider.PROVIDER_ID, + "Github", + R.layout.fui_idp_button_github + ) { + /** + * Specifies the additional permissions to be requested. + * + * Available permissions can be found + * [here](https://developer.github.com/apps/building-oauth-apps/scopes-for-oauth-apps/#available-scopes). + * + * @deprecated Please use [setScopes] instead. + */ + @Deprecated("Please use setScopes instead", ReplaceWith("setScopes(permissions)")) + fun setPermissions(permissions: List): GitHubBuilder { + setScopes(permissions) + return this + } + } + + /** + * [IdpConfig] builder for the Apple provider. + */ + class AppleBuilder : GenericOAuthProviderBuilder( + APPLE_PROVIDER, + "Apple", + R.layout.fui_idp_button_apple + ) + + /** + * [IdpConfig] builder for the Microsoft provider. + */ + class MicrosoftBuilder : GenericOAuthProviderBuilder( + MICROSOFT_PROVIDER, + "Microsoft", + R.layout.fui_idp_button_microsoft + ) + + /** + * [IdpConfig] builder for the Yahoo provider. + */ + class YahooBuilder : GenericOAuthProviderBuilder( + YAHOO_PROVIDER, + "Yahoo", + R.layout.fui_idp_button_yahoo + ) + + /** + * [IdpConfig] builder for a Generic OAuth provider. + */ + open class GenericOAuthProviderBuilder( + providerId: String, + providerName: String, + buttonId: Int + ) : Builder(providerId) { + + init { + requireNotNull(providerId) { "The provider ID cannot be null." } + requireNotNull(providerName) { "The provider name cannot be null." } + + mParams.putString(ExtraConstants.GENERIC_OAUTH_PROVIDER_ID, providerId) + mParams.putString(ExtraConstants.GENERIC_OAUTH_PROVIDER_NAME, providerName) + mParams.putInt(ExtraConstants.GENERIC_OAUTH_BUTTON_ID, buttonId) + } + + fun setScopes(scopes: List): GenericOAuthProviderBuilder { + mParams.putStringArrayList( + ExtraConstants.GENERIC_OAUTH_SCOPES, + ArrayList(scopes) + ) + return this + } + + fun setCustomParameters( + customParameters: Map + ): GenericOAuthProviderBuilder { + mParams.putSerializable( + ExtraConstants.GENERIC_OAUTH_CUSTOM_PARAMETERS, + HashMap(customParameters) + ) + return this + } + } +} + +/** + * The entry point to the AuthUI authentication flow, and related utility methods. If your + * application uses the default [FirebaseApp] instance, an AuthUI instance can be retrieved + * simply by calling [AuthUI.getInstance]. If an alternative app instance is in use, call + * [AuthUI.getInstance] instead, passing the appropriate app instance. + * + * See the + * [README](https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#table-of-contents) + * for examples on how to get started with FirebaseUI Auth. + */ +class AuthUI private constructor(private val mApp: FirebaseApp) { + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + companion object { + const val TAG = "AuthUI" + + /** + * Provider for anonymous users. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + const val ANONYMOUS_PROVIDER = com.firebase.ui.auth.ANONYMOUS_PROVIDER + const val EMAIL_LINK_PROVIDER = com.firebase.ui.auth.EMAIL_LINK_PROVIDER + const val MICROSOFT_PROVIDER = com.firebase.ui.auth.MICROSOFT_PROVIDER + const val YAHOO_PROVIDER = com.firebase.ui.auth.YAHOO_PROVIDER + const val APPLE_PROVIDER = com.firebase.ui.auth.APPLE_PROVIDER + + /** + * Default value for logo resource, omits the logo from the [AuthMethodPickerActivity]. + */ + const val NO_LOGO = -1 + + /** + * The set of authentication providers supported in Firebase Auth UI. + */ + @JvmField + val SUPPORTED_PROVIDERS: Set = Collections.unmodifiableSet( + HashSet( + listOf( + 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). + */ + @JvmField + val SUPPORTED_OAUTH_PROVIDERS: Set = Collections.unmodifiableSet( + HashSet( + listOf( + 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) + @JvmField + val SOCIAL_PROVIDERS: Set = Collections.unmodifiableSet( + HashSet( + listOf( + GoogleAuthProvider.PROVIDER_ID, + FacebookAuthProvider.PROVIDER_ID + ) + ) + ) + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + const val UNCONFIGURED_CONFIG_VALUE = "CHANGE-ME" + + private val INSTANCES = IdentityHashMap() + + private var sApplicationContext: Context? = null + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @JvmStatic + fun getApplicationContext(): Context { + return sApplicationContext!! + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @JvmStatic + fun setApplicationContext(context: Context) { + sApplicationContext = Preconditions.checkNotNull(context, "App context cannot be null.") + .applicationContext + } + + /** + * Retrieves the [AuthUI] instance associated with the default app, as returned by + * [FirebaseApp.getInstance]. + * + * @throws IllegalStateException if the default app is not initialized. + */ + @JvmStatic + fun getInstance(): AuthUI { + return getInstance(FirebaseApp.getInstance()) + } + + /** + * Retrieves the [AuthUI] instance associated the the specified app name. + * + * @throws IllegalStateException if the app is not initialized. + */ + @JvmStatic + fun getInstance(appName: String): AuthUI { + return getInstance(FirebaseApp.getInstance(appName)) + } + + /** + * Retrieves the [AuthUI] instance associated the the specified app. + */ + @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 + } + } + + /** + * 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 [SignInIntentBuilder.setEmailLink] before starting AuthUI + * and it will be handled immediately. + */ + @JvmStatic + fun canHandleIntent(intent: Intent?): Boolean { + if (intent?.data == null) { + return false + } + val link = intent.data.toString() + return FirebaseAuth.getInstance().isSignInWithEmailLink(link) + } + + /** + * Default theme used by [SignInIntentBuilder.setTheme] if no theme customization is + * required. + */ + @JvmStatic + @StyleRes + fun getDefaultTheme(): Int { + return R.style.FirebaseUI_DefaultMaterialTheme + } + } + + private val mAuth: FirebaseAuth = FirebaseAuth.getInstance(mApp) + private var mEmulatorHost: String? = null + private var mEmulatorPort: Int = -1 + + init { + try { + mAuth.setFirebaseUIVersion(BuildConfig.VERSION_NAME) + } catch (e: Exception) { + Log.e(TAG, "Couldn't set the FUI version.", e) + } + mAuth.useAppLanguage() + } + + /** + * 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 + * ([Task.isSuccessful], or that the sign-out attempt failed unexpectedly + * ![Task.isSuccessful]). + */ + 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 + } + } + + /** + * Delete the user from FirebaseAuth. + * + * Any associated saved credentials are not explicitly deleted with the new APIs. + * + * @param context the calling [Context]. + */ + fun delete(context: Context): Task { + val currentUser = mAuth.currentUser + ?: return Tasks.forException( + FirebaseAuthInvalidUserException( + CommonStatusCodes.SIGN_IN_REQUIRED.toString(), + "No currently signed in user." + ) + ) + + return signOutIdps(context).continueWithTask { task -> + task.result // Propagate exception if there was one. + currentUser.delete() + } + } + + /** + * Connect to the Firebase Authentication emulator. + * @see FirebaseAuth.useEmulator + */ + fun useEmulator(host: String, port: Int) { + require(port in 0..65535) { "Port must be between 0 and 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) + } + } + + /** + * Starts the process of creating a sign in intent, with the mandatory application context + * parameter. + */ + fun createSignInIntentBuilder(): SignInIntentBuilder { + return SignInIntentBuilder() + } + + /** + * Base builder for both [SignInIntentBuilder]. + */ + abstract class AuthIntentBuilder> { + private val app: FirebaseApp = mApp + protected val mProviders = ArrayList() + protected var mDefaultProvider: IdpConfig? = null + protected var mLogo = NO_LOGO + protected var mTheme = getDefaultTheme() + protected var mTosUrl: String? = null + protected var mPrivacyPolicyUrl: String? = null + protected var mAlwaysShowProviderChoice = false + protected var mLockOrientation = false + protected var mEnableCredentials = true + protected var mAuthMethodPickerLayout: AuthMethodPickerLayout? = null + protected var mPasswordSettings: ActionCodeSettings? = null + + /** + * Specifies the theme to use for the application flow. If no theme is specified, a + * default theme will be used. + */ + @Suppress("UNCHECKED_CAST") + fun setTheme(@StyleRes theme: Int): T { + mTheme = Preconditions.checkValidStyle( + app.applicationContext, + theme, + "theme identifier is unknown or not a style definition" + ) + return this as T + } + + /** + * Specifies the logo to use for the [AuthMethodPickerActivity]. If no logo is + * specified, none will be used. + */ + @Suppress("UNCHECKED_CAST") + fun setLogo(@DrawableRes logo: Int): T { + mLogo = logo + return this as T + } + + /** + * Specifies the terms-of-service URL for the application. + * + * @deprecated Please use [setTosAndPrivacyPolicyUrls] For the Tos + * link to be displayed a Privacy Policy url must also be provided. + */ + @Deprecated( + "Please use setTosAndPrivacyPolicyUrls. For the Tos link to be displayed a Privacy " + + "Policy url must also be provided.", + ReplaceWith("setTosAndPrivacyPolicyUrls") + ) + @Suppress("UNCHECKED_CAST") + fun setTosUrl(tosUrl: String?): T { + mTosUrl = tosUrl + return this as T + } + + /** + * Specifies the privacy policy URL for the application. + * + * @deprecated Please use [setTosAndPrivacyPolicyUrls] For the + * Privacy Policy link to be displayed a Tos url must also be provided. + */ + @Deprecated( + "Please use setTosAndPrivacyPolicyUrls. For the Privacy Policy link to be " + + "displayed a Tos url must also be provided.", + ReplaceWith("setTosAndPrivacyPolicyUrls") + ) + @Suppress("UNCHECKED_CAST") + fun setPrivacyPolicyUrl(privacyPolicyUrl: String?): T { + mPrivacyPolicyUrl = privacyPolicyUrl + return this as T + } + + @Suppress("UNCHECKED_CAST") + fun setTosAndPrivacyPolicyUrls(tosUrl: String, privacyPolicyUrl: String): T { + requireNotNull(tosUrl) { "tosUrl cannot be null" } + requireNotNull(privacyPolicyUrl) { "privacyPolicyUrl cannot be null" } + mTosUrl = tosUrl + mPrivacyPolicyUrl = privacyPolicyUrl + return this as T + } + + /** + * 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 [IdpConfig]s, where each [IdpConfig] contains the + * configuration parameters for the IDP. + * @throws IllegalStateException if anonymous provider is the only specified provider. + * @see IdpConfig + */ + @Suppress("UNCHECKED_CAST") + fun setAvailableProviders(idpConfigs: List): T { + requireNotNull(idpConfigs) { "idpConfigs cannot be null" } + require(!(idpConfigs.size == 1 && idpConfigs[0].getProviderId() == ANONYMOUS_PROVIDER)) { + "Sign in as guest cannot be the only sign in method. In this case, sign the user " + + "in anonymously yourself; no UI is needed." + } + + mProviders.clear() + + for (config in idpConfigs) { + require(!mProviders.contains(config)) { + "Each provider can only be set once. ${config.getProviderId()} was set twice." + } + mProviders.add(config) + } + + return this as T + } + + /** + * Specifies the default authentication provider, bypassing the provider selection screen. + * The provider here must already be included via [setAvailableProviders], and + * this method is incompatible with [setAlwaysShowSignInMethodScreen]. + * + * @param config the default [IdpConfig] to use. + */ + @Suppress("UNCHECKED_CAST") + fun setDefaultProvider(config: IdpConfig?): T { + if (config != null) { + require(mProviders.contains(config)) { + "Default provider not in available providers list." + } + require(!mAlwaysShowProviderChoice) { + "Can't set default provider and always show provider choice." + } + } + mDefaultProvider = config + return this as T + } + + /** + * Enables or disables the use of Credential Manager for Passwords credential selector + * + * Is enabled by default. + * + * @param enableCredentials enables credential selector before signup + */ + @Suppress("UNCHECKED_CAST") + fun setCredentialManagerEnabled(enableCredentials: Boolean): T { + mEnableCredentials = enableCredentials + return this as T + } + + /** + * Set a custom layout for the AuthMethodPickerActivity screen. + * See [AuthMethodPickerLayout]. + * + * @param authMethodPickerLayout custom layout descriptor object. + */ + @Suppress("UNCHECKED_CAST") + fun setAuthMethodPickerLayout(authMethodPickerLayout: AuthMethodPickerLayout): T { + mAuthMethodPickerLayout = authMethodPickerLayout + return this as T + } + + /** + * 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. + */ + @Suppress("UNCHECKED_CAST") + fun setAlwaysShowSignInMethodScreen(alwaysShow: Boolean): T { + require(!(alwaysShow && mDefaultProvider != null)) { + "Can't show provider choice with a default provider." + } + mAlwaysShowProviderChoice = alwaysShow + return this as T + } + + /** + * 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. + */ + @Suppress("UNCHECKED_CAST") + fun setLockOrientation(lockOrientation: Boolean): T { + mLockOrientation = lockOrientation + return this as T + } + + /** + * Set custom settings for the RecoverPasswordActivity. + * + * @param passwordSettings to allow additional state via a continue URL. + */ + @Suppress("UNCHECKED_CAST") + fun setResetPasswordSettings(passwordSettings: ActionCodeSettings): T { + mPasswordSettings = passwordSettings + return this as T + } + + open fun build(): Intent { + if (mProviders.isEmpty()) { + mProviders.add(IdpConfig.EmailBuilder().build()) + } + + return KickoffActivity.createIntent(app.applicationContext, getFlowParams()) + } + + protected abstract fun getFlowParams(): FlowParameters + } + + /** + * Builder for the intent to start the user authentication flow. + */ + inner class SignInIntentBuilder : AuthIntentBuilder() { + private var mEmailLink: String? = null + private var mEnableAnonymousUpgrade = false + + /** + * Specifies the email link to be used for sign in. When set, a sign in attempt will be + * made immediately. + */ + fun setEmailLink(emailLink: String): SignInIntentBuilder { + 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. + */ + fun enableAnonymousUsersAutoUpgrade(): SignInIntentBuilder { + mEnableAnonymousUpgrade = true + validateEmailBuilderConfig() + return this + } + + private fun validateEmailBuilderConfig() { + for (config in mProviders) { + if (config.getProviderId() == EMAIL_LINK_PROVIDER) { + val emailLinkForceSameDevice = + config.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE, true) + require(emailLinkForceSameDevice) { + "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 + ) + } + } +} \ 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