Skip to content

feat(auth): migrate to Kotlin #2189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: version-9.0.0-dev
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}

1,402 changes: 0 additions & 1,402 deletions auth/src/main/java/com/firebase/ui/auth/AuthUI.java

This file was deleted.

889 changes: 889 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/AuthUI.kt

Large diffs are not rendered by default.

144 changes: 0 additions & 144 deletions auth/src/main/java/com/firebase/ui/auth/ErrorCodes.java

This file was deleted.

124 changes: 124 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/ErrorCodes.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
2 changes: 1 addition & 1 deletion auth/src/main/java/com/firebase/ui/auth/IdpResponse.java
Original file line number Diff line number Diff line change
@@ -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.");
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
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<out String>?): Int = 0

override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int = 0
}
130 changes: 0 additions & 130 deletions auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.java

This file was deleted.

75 changes: 75 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/data/model/CountryInfo.kt
Original file line number Diff line number Diff line change
@@ -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<CountryInfo>, 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<CountryInfo> = object : Parcelable.Creator<CountryInfo> {
override fun createFromParcel(source: Parcel): CountryInfo = CountryInfo(source)
override fun newArray(size: Int): Array<CountryInfo?> = 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)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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?
)
223 changes: 0 additions & 223 deletions auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.java

This file was deleted.

152 changes: 152 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/data/model/FlowParameters.kt
Original file line number Diff line number Diff line change
@@ -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<IdpConfig>,
@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<IdpConfig> =
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<FlowParameters> {
override fun createFromParcel(parcel: Parcel): FlowParameters {
return FlowParameters(parcel)
}

override fun newArray(size: Int): Array<FlowParameters?> {
return arrayOfNulls(size)
}

@JvmStatic
fun fromIntent(intent: Intent): FlowParameters =
// getParcelableExtra returns a nullable type so we use !! to mirror the Java behavior.
intent.getParcelableExtra<FlowParameters>(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]
}
}
Original file line number Diff line number Diff line change
@@ -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<IdpResponse>(
IntentRequiredException(
PhoneActivity.createIntent(app, arguments, firstIdpConfig.params),
PhoneActivity.createIntent(app, arguments, firstIdpConfig.getParams()),
RequestCodes.PHONE_FLOW
)
)

This file was deleted.

188 changes: 188 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}

This file was deleted.

106 changes: 106 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailHandler.kt
Original file line number Diff line number Diff line change
@@ -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<User>(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))
}
}
}
247 changes: 0 additions & 247 deletions auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.java

This file was deleted.

296 changes: 296 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.kt
Original file line number Diff line number Diff line change
@@ -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())
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<IdpResponse>(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)
}
}
}
}
Loading