Skip to content

Commit 7fdf785

Browse files
lsiracsamtstern
authored andcommitted
Email link Phase 2 - Cross device flows (#1522)
1 parent 60bc8e1 commit 7fdf785

37 files changed

+1517
-213
lines changed

auth/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,10 @@ if (AuthUI.canHandleIntent(getIntent())) {
390390
}
391391
```
392392

393-
Note that email link sign in is currently only supported for the same device. Finishing the flow on a different device will result
394-
in the user being shown an error.
393+
#### Cross device support
394+
395+
We support cross device email link sign in for the normal flows. It is not supported with anonymous user upgrade. By default,
396+
cross device support is enabled. You can disable it by calling `setForceSameDevice` on the `EmailBuilder` instance.
395397

396398
##### Adding a ToS and privacy policy
397399

auth/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@
7878
android:theme="@style/FirebaseUI.Transparent"
7979
android:windowSoftInputMode="adjustResize" />
8080

81+
<activity
82+
android:name=".ui.email.EmailLinkErrorRecoveryActivity"
83+
android:label="@string/fui_sign_in_default"
84+
android:exported="false"
85+
android:windowSoftInputMode="adjustResize" />
86+
8187
<activity
8288
android:name=".ui.idp.AuthMethodPickerActivity"
8389
android:label="@string/fui_default_toolbar_title"

auth/src/main/java/com/firebase/ui/auth/AuthUI.java

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -612,29 +612,61 @@ public EmailBuilder setRequireName(boolean requireName) {
612612
return this;
613613
}
614614

615+
/**
616+
* Enables email link sign in instead of password based sign in. Once enabled, you must
617+
* pass a valid {@link ActionCodeSettings} object using
618+
* {@link #setActionCodeSettings(ActionCodeSettings)}
619+
* <p>
620+
* You must enable Firebase Dynamic Links in the Firebase Console to use email link
621+
* sign in.
622+
*
623+
* @throws IllegalStateException if {@link ActionCodeSettings} is null or not
624+
* provided with email link enabled.
625+
*/
615626
@NonNull
616627
public EmailBuilder enableEmailLinkSignIn() {
617628
setProviderId(EMAIL_LINK_PROVIDER);
618629
return this;
619630
}
620631

632+
/**
633+
* Sets the {@link ActionCodeSettings} object to be used for email link sign in.
634+
* <p>
635+
* {@link ActionCodeSettings#canHandleCodeInApp()} must be set to true, and a valid
636+
* continueUrl must be passed via {@link ActionCodeSettings.Builder#setUrl(String)}.
637+
* This URL must be whitelisted in the Firebase Console.
638+
*
639+
* @throws IllegalStateException if canHandleCodeInApp is set to false
640+
* @throws NullPointerException if ActionCodeSettings is null
641+
*/
621642
@NonNull
622643
public EmailBuilder setActionCodeSettings(ActionCodeSettings actionCodeSettings) {
623644
getParams().putParcelable(ExtraConstants.ACTION_CODE_SETTINGS, actionCodeSettings);
624645
return this;
625646
}
626647

648+
/**
649+
* Disables allowing email link sign in to occur across different devices.
650+
* <p>
651+
* This cannot be disabled with anonymous upgrade.
652+
*/
653+
@NonNull
654+
public EmailBuilder setForceSameDevice() {
655+
getParams().putBoolean(ExtraConstants.FORCE_SAME_DEVICE, true);
656+
return this;
657+
}
658+
627659
@Override
628660
public IdpConfig build() {
629661
if (super.mProviderId.equals(EMAIL_LINK_PROVIDER)) {
630-
ActionCodeSettings actionCodeSettings = getParams().getParcelable
631-
(ExtraConstants.ACTION_CODE_SETTINGS);
662+
ActionCodeSettings actionCodeSettings =
663+
getParams().getParcelable(ExtraConstants.ACTION_CODE_SETTINGS);
632664
Preconditions.checkNotNull(actionCodeSettings, "ActionCodeSettings cannot be " +
633665
"null when using email link sign in.");
634-
if (actionCodeSettings != null && !actionCodeSettings.canHandleCodeInApp()) {
666+
if (!actionCodeSettings.canHandleCodeInApp()) {
635667
// Pre-emptively fail if actionCodeSettings are misconfigured. This would
636668
// have happened when calling sendSignInLinkToEmail
637-
throw new IllegalArgumentException(
669+
throw new IllegalStateException(
638670
"You must set canHandleCodeInApp in your ActionCodeSettings to " +
639671
"true for Email-Link Sign-in.");
640672
}
@@ -1247,6 +1279,7 @@ public T setAuthMethodPickerLayout(@NonNull AuthMethodPickerLayout authMethodPic
12471279
* a single provider configured.
12481280
* <p>
12491281
* <p>This is false by default.
1282+
*
12501283
* @param alwaysShow if true, force the sign-in choice screen to show.
12511284
*/
12521285
@NonNull
@@ -1293,13 +1326,31 @@ public SignInIntentBuilder setEmailLink(@NonNull final String emailLink) {
12931326
/**
12941327
* Enables upgrading anonymous accounts to full accounts during the sign-in flow.
12951328
* This is disabled by default.
1329+
*
1330+
* @throws IllegalStateException when you attempt to enable anonymous user upgrade
1331+
* without forcing the same device flow for email link sign in.
12961332
*/
12971333
@NonNull
12981334
public SignInIntentBuilder enableAnonymousUsersAutoUpgrade() {
12991335
mEnableAnonymousUpgrade = true;
1336+
validateEmailBuilderConfig();
13001337
return this;
13011338
}
13021339

1340+
private void validateEmailBuilderConfig() {
1341+
for (int i = 0; i < mProviders.size(); i++) {
1342+
IdpConfig config = mProviders.get(i);
1343+
if (config.getProviderId().equals(EMAIL_LINK_PROVIDER)) {
1344+
boolean emailLinkForceSameDevice =
1345+
config.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE, true);
1346+
if (!emailLinkForceSameDevice) {
1347+
throw new IllegalStateException("You must force the same device flow " +
1348+
"when using email link sign in with anonymous user upgrade");
1349+
}
1350+
}
1351+
}
1352+
}
1353+
13031354
@Override
13041355
protected FlowParameters getFlowParams() {
13051356
return new FlowParameters(

auth/src/main/java/com/firebase/ui/auth/ErrorCodes.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public final class ErrorCodes {
3636
*/
3737
public static final int ANONYMOUS_UPGRADE_MERGE_CONFLICT = 5;
3838
/**
39-
* Signing in with a different email in the WelcomeBackIdp flow.
39+
* Signing in with a different email in the WelcomeBackIdp flow or email link flow.
4040
*/
4141
public static final int EMAIL_MISMATCH_ERROR = 6;
4242
/**
@@ -49,7 +49,16 @@ public final class ErrorCodes {
4949
*/
5050
public static final int EMAIL_LINK_WRONG_DEVICE_ERROR = 8;
5151

52+
/**
53+
* We need to prompt the user for their email.
54+
* */
55+
public static final int EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR = 9;
5256

57+
/**
58+
* Cross device linking flow - we need to ask the user if they want to continue linking or
59+
* just sign in.
60+
* */
61+
public static final int EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR = 10;
5362

5463
private ErrorCodes() {
5564
throw new AssertionError("No instance for you!");
@@ -76,8 +85,12 @@ public static String toFriendlyMessage(@Code int code) {
7685
"provided";
7786
case INVALID_EMAIL_LINK_ERROR:
7887
return "You are are attempting to sign in with an invalid email link";
88+
case EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR:
89+
return "Please enter your email to continue signing in";
7990
case EMAIL_LINK_WRONG_DEVICE_ERROR:
8091
return "You must open the email link on the same device.";
92+
case EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR:
93+
return "You must determine if you want to continue linking or complete the sign in";
8194
default:
8295
throw new IllegalArgumentException("Unknown code: " + code);
8396
}
@@ -95,7 +108,9 @@ public static String toFriendlyMessage(@Code int code) {
95108
ANONYMOUS_UPGRADE_MERGE_CONFLICT,
96109
EMAIL_MISMATCH_ERROR,
97110
INVALID_EMAIL_LINK_ERROR,
98-
EMAIL_LINK_WRONG_DEVICE_ERROR
111+
EMAIL_LINK_WRONG_DEVICE_ERROR,
112+
EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR,
113+
EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR
99114
})
100115
@Retention(RetentionPolicy.SOURCE)
101116
public @interface Code {

auth/src/main/java/com/firebase/ui/auth/ui/AppCompatBase.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
package com.firebase.ui.auth.ui;
1616

1717
import android.os.Bundle;
18+
import android.support.annotation.NonNull;
1819
import android.support.annotation.Nullable;
1920
import android.support.annotation.RestrictTo;
21+
import android.support.v4.app.Fragment;
22+
import android.support.v4.app.FragmentTransaction;
2023

2124
import com.firebase.ui.auth.R;
2225

@@ -28,4 +31,25 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
2831
setTheme(R.style.FirebaseUI); // Provides default values
2932
setTheme(getFlowParams().themeId);
3033
}
34+
35+
protected void switchFragment(@NonNull Fragment fragment,
36+
int fragmentId,
37+
@NonNull String tag,
38+
boolean withTransition,
39+
boolean addToBackStack) {
40+
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
41+
if (withTransition) {
42+
ft.setCustomAnimations(R.anim.fui_slide_in_right, R.anim.fui_slide_out_left);
43+
}
44+
ft.replace(fragmentId, fragment, tag);
45+
if (addToBackStack) {
46+
ft.addToBackStack(null).commit();
47+
} else {
48+
ft.disallowAddToBackStack().commit();
49+
}
50+
}
51+
52+
protected void switchFragment(@NonNull Fragment fragment, int fragmentId, @NonNull String tag) {
53+
switchFragment(fragment, fragmentId, tag, false, false);
54+
}
3155
}

auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
7979
mEmailLayout.setOnClickListener(this);
8080
mEmailEditText.setOnClickListener(this);
8181

82+
// Hide header
83+
TextView headerText = view.findViewById(R.id.header_text);
84+
if (headerText != null) {
85+
headerText.setVisibility(View.GONE);
86+
}
87+
8288
ImeHelper.setImeOnDoneListener(mEmailEditText, this);
8389

8490
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && getFlowParams().enableHints) {

auth/src/main/java/com/firebase/ui/auth/ui/email/EmailActivity.java

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import android.support.annotation.RestrictTo;
2222
import android.support.annotation.StringRes;
2323
import android.support.design.widget.TextInputLayout;
24-
import android.support.v4.app.Fragment;
2524
import android.support.v4.app.FragmentTransaction;
2625
import android.support.v4.view.ViewCompat;
2726

@@ -92,12 +91,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
9291
EmailLinkPersistenceManager.getInstance().saveIdpResponseForLinking(getApplication(),
9392
responseForLinking);
9493

95-
EmailLinkFragment fragment = EmailLinkFragment.newInstance(email, actionCodeSettings);
96-
switchFragment(fragment, EmailLinkFragment.TAG);
94+
boolean forceSameDevice =
95+
emailConfig.getParams().getBoolean(ExtraConstants.FORCE_SAME_DEVICE);
96+
EmailLinkFragment fragment = EmailLinkFragment.newInstance(email, actionCodeSettings,
97+
responseForLinking, forceSameDevice);
98+
switchFragment(fragment, R.id.fragment_register_email, EmailLinkFragment.TAG);
9799
} else {
98100
// Start with check email
99101
CheckEmailFragment fragment = CheckEmailFragment.newInstance(email);
100-
switchFragment(fragment, CheckEmailFragment.TAG);
102+
switchFragment(fragment, R.id.fragment_register_email, CheckEmailFragment.TAG);
101103
}
102104
}
103105

@@ -128,7 +130,6 @@ this, getFlowParams(), new IdpResponse.Builder(user).build()),
128130

129131
@Override
130132
public void onExistingIdpUser(User user) {
131-
132133
// Existing social user, direct them to sign in using their chosen provider.
133134
startActivityForResult(
134135
WelcomeBackIdpPrompt.createIntent(this, getFlowParams(), user),
@@ -173,7 +174,8 @@ public void onNewUser(User user) {
173174
public void onTroubleSigningIn(String email) {
174175
TroubleSigningInFragment troubleSigningInFragment = TroubleSigningInFragment.newInstance
175176
(email);
176-
switchFragment(troubleSigningInFragment, TroubleSigningInFragment.TAG, true, true);
177+
switchFragment(troubleSigningInFragment, R.id.fragment_register_email,
178+
TroubleSigningInFragment.TAG, true, true);
177179
}
178180

179181
@Override
@@ -218,28 +220,7 @@ private void showRegisterEmailLinkFragment(AuthUI.IdpConfig emailConfig,
218220
(ExtraConstants.ACTION_CODE_SETTINGS);
219221
EmailLinkFragment fragment = EmailLinkFragment.newInstance(email,
220222
actionCodeSettings);
221-
switchFragment(fragment, EmailLinkFragment.TAG);
222-
}
223-
224-
225-
private void switchFragment(Fragment fragment,
226-
String tag,
227-
boolean withTransition,
228-
boolean addToBackStack) {
229-
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
230-
if (withTransition) {
231-
ft.setCustomAnimations(R.anim.fui_slide_in_right, R.anim.fui_slide_out_left);
232-
}
233-
ft.replace(R.id.fragment_register_email, fragment, tag);
234-
if (addToBackStack) {
235-
ft.addToBackStack(null).commit();
236-
} else {
237-
ft.disallowAddToBackStack().commit();
238-
}
239-
}
240-
241-
private void switchFragment(Fragment fragment, String tag) {
242-
switchFragment(fragment, tag, false, false);
223+
switchFragment(fragment, R.id.fragment_register_email, EmailLinkFragment.TAG);
243224
}
244225

245226
@Override

auth/src/main/java/com/firebase/ui/auth/ui/email/EmailLinkCatcherActivity.java

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.firebase.ui.auth.viewmodel.RequestCodes;
2323
import com.firebase.ui.auth.viewmodel.ResourceObserver;
2424
import com.firebase.ui.auth.viewmodel.email.EmailLinkSignInHandler;
25+
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
2526

2627
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
2728
public class EmailLinkCatcherActivity extends InvisibleActivityBase {
@@ -35,6 +36,7 @@ public static Intent createIntent(Context context, FlowParameters flowParams) {
3536
@Override
3637
protected void onCreate(@Nullable Bundle savedInstanceState) {
3738
super.onCreate(savedInstanceState);
39+
3840
initHandler();
3941

4042
if (getFlowParams().emailLink != null) {
@@ -59,23 +61,42 @@ protected void onFailure(@NonNull final Exception e) {
5961
IdpResponse res = ((FirebaseAuthAnonymousUpgradeException) e).getResponse();
6062
finish(RESULT_CANCELED, new Intent().putExtra(ExtraConstants
6163
.IDP_RESPONSE, res));
62-
} else {
63-
if (e instanceof FirebaseUiException) {
64-
if (((FirebaseUiException) e).getErrorCode() == ErrorCodes
65-
.EMAIL_LINK_WRONG_DEVICE_ERROR) {
66-
buildAlertDialog(ErrorCodes.EMAIL_LINK_WRONG_DEVICE_ERROR).show();
67-
} else if (((FirebaseUiException) e).getErrorCode() == ErrorCodes
68-
.INVALID_EMAIL_LINK_ERROR) {
69-
buildAlertDialog(ErrorCodes.INVALID_EMAIL_LINK_ERROR).show();
70-
}
71-
} else {
72-
finish(RESULT_CANCELED, IdpResponse.getErrorIntent(e));
64+
} else if (e instanceof FirebaseUiException) {
65+
int errorCode = ((FirebaseUiException) e).getErrorCode();
66+
if (errorCode == ErrorCodes.EMAIL_LINK_WRONG_DEVICE_ERROR
67+
|| errorCode == ErrorCodes.INVALID_EMAIL_LINK_ERROR) {
68+
buildAlertDialog(errorCode).show();
69+
} else if (errorCode == ErrorCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_ERROR
70+
|| errorCode == ErrorCodes.EMAIL_MISMATCH_ERROR) {
71+
startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW);
72+
} else if (errorCode == ErrorCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_ERROR) {
73+
startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW);
7374
}
75+
} else if (e instanceof FirebaseAuthInvalidCredentialsException) {
76+
startErrorRecoveryFlow(RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW);
77+
} else {
78+
finish(RESULT_CANCELED, IdpResponse.getErrorIntent(e));
7479
}
7580
}
7681
});
7782
}
7883

84+
/**
85+
* @param flow must be one of RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW or
86+
* RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW
87+
*/
88+
private void startErrorRecoveryFlow(int flow) {
89+
if (flow != RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW
90+
&& flow != RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW) {
91+
throw new IllegalStateException("Invalid flow param. It must be either " +
92+
"RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW or " +
93+
"RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW");
94+
}
95+
Intent intent = EmailLinkErrorRecoveryActivity.createIntent(getApplicationContext(),
96+
getFlowParams(), flow);
97+
startActivityForResult(intent, flow);
98+
}
99+
79100
private AlertDialog buildAlertDialog(int errorCode) {
80101
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
81102
if (errorCode == ErrorCodes.EMAIL_LINK_WRONG_DEVICE_ERROR) {
@@ -99,4 +120,19 @@ public void onClick(DialogInterface dialog, int id) {
99120
}
100121
return alertDialog.create();
101122
}
123+
124+
@Override
125+
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
126+
if (requestCode == RequestCodes.EMAIL_LINK_PROMPT_FOR_EMAIL_FLOW
127+
|| requestCode == RequestCodes.EMAIL_LINK_CROSS_DEVICE_LINKING_FLOW) {
128+
IdpResponse response = IdpResponse.fromResultIntent(data);
129+
// CheckActionCode is called before starting this flow, so we only get here
130+
// if the sign in link is valid - it can only fail by being cancelled.
131+
if (resultCode == RESULT_OK) {
132+
finish(RESULT_OK, response.toIntent());
133+
} else {
134+
finish(RESULT_CANCELED, null);
135+
}
136+
}
137+
}
102138
}

0 commit comments

Comments
 (0)