Skip to content

Completely rewrite provider login with new architecture #1189

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

Merged
merged 41 commits into from
Mar 23, 2018
Merged

Completely rewrite provider login with new architecture #1189

merged 41 commits into from
Mar 23, 2018

Conversation

SUPERCILEX
Copy link
Collaborator

@SUPERCILEX SUPERCILEX commented Mar 16, 2018

Built on top of #1188

Fixes #1200, fixes #1201

Signed-off-by: Alex Saveau <[email protected]>
Signed-off-by: Alex Saveau <[email protected]>
Signed-off-by: Alex Saveau <[email protected]>
@SUPERCILEX SUPERCILEX requested a review from samtstern as a code owner March 16, 2018 01:28
@@ -0,0 +1,153 @@
package com.firebase.ui.auth.data.remote;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO(me): add tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just for review sanity you should do the tests in another PR. This one is already 1k+ lines and tests will probably double it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM for the individual provider handlers since they work AFAIK, but I've actually found a bunch of issues with my ProvidersHandler implementation thanks to some lovely TDD (so I'll just add that one for now).

@SUPERCILEX
Copy link
Collaborator Author

@samtstern This is going to be the biggest PR by far since it requires major refactors across a large portion of the FUI API surface. The basic ideas are as follows:

  1. Providers are now middlemen to talk between the UI layer and their "handlers".
  2. Each handler is in charge of processing whatever work needs to be done in the background or otherwise to generate an IdpResponse which is then reported back to the provider middleman who then reports it back to the activity scoped provider handler who's then in charge of signing the user in and reporting that result back to the activity. Whew! It's quite a long chain, but it makes sure everything is cleanly separated and no memory leaks can occur.
  3. IdpSignInContainer is dead! 👏 It's been replaced with a dead-simple activity inspired from the credentials one you wrote.

Signed-off-by: Alex Saveau <[email protected]>
Signed-off-by: Alex Saveau <[email protected]>
Signed-off-by: Alex Saveau <[email protected]>
Signed-off-by: Alex Saveau <[email protected]>
Signed-off-by: Alex Saveau <[email protected]>
@samtstern
Copy link
Contributor

@SUPERCILEX well this is up next, after merges are resolved. Right?

# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java
#	auth/src/main/java/com/firebase/ui/auth/ui/idp/WelcomeBackIdpPrompt.java
#	auth/src/main/java/com/firebase/ui/auth/util/signincontainer/IdpSignInContainer.java
#	auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SignInDelegate.java
#	auth/src/main/java/com/firebase/ui/auth/viewmodel/AuthViewModelBase.java
#	auth/src/main/java/com/firebase/ui/auth/viewmodel/smartlock/SmartLockHandler.java
@SUPERCILEX
Copy link
Collaborator Author

@samtstern Yep, I'm working on tests right now.

Signed-off-by: Alex Saveau <[email protected]>
Copy link
Contributor

@samtstern samtstern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started reviewing but then i got pulled away into something else. Sending the comments I had so far, will probably pick this up on Monday.

@@ -276,7 +276,7 @@ public static int getDefaultTheme() {
.disableAutoSignIn()
.continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
public Task<Void> then(@NonNull Task<Void> task) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some linter tell you not to have throws Exception here? Does it actually change anything to remove it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, IntelliJ has been removing these, not sure why, but it doesn't seem to matter.

import com.firebase.ui.auth.viewmodel.idp.ProviderHandler;
import com.firebase.ui.auth.viewmodel.idp.ProvidersHandler;

public final class FacebookParams extends ProviderHandler.ParamsBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RestrictTo? Also class comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm actually hoping to get rid of these. They're in a separate class because the compiler won't let me make them a static inner...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems extracting the base class fixes it.

import com.firebase.ui.auth.viewmodel.idp.ProviderHandler;
import com.firebase.ui.auth.viewmodel.idp.ProvidersHandler;

public final class GoogleParams extends ProviderHandler.ParamsBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here and on all other new classes:

  • Double-check visibility restrictions.
  • Add class comment.

GoogleSignIn.getClient(getApplication(), getSignInOptions()).getSignInIntent()));
}

public LiveData<Resource<Intent>> getRequest() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this is a Resource? It only ever seems to be in the SUCCESS state (or null).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using it for the isUsed() check, do you think we should extract it into a seperate, contained class? The main pain-point is that I'm trying to keep the previous behavior which included starting the sign-in process from onActivityResult if it's an invalid account.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a Resource<Void> with the Intent being part of an error like in other cases then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, why not? It seems a little better than what I have.

.getResult(ApiException.class);
setResult(createIdpResponse(account));
} catch (ApiException e) {
if (e.getStatusCode() == CommonStatusCodes.INVALID_ACCOUNT) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This took me a minute to understand, so maybe add:

// If we get INVALID_ACCOUNT it means the pre-set account was not available on the device, set email to null
// and try to launch sign-in again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

import com.firebase.ui.auth.data.model.FlowParameters;
import com.firebase.ui.auth.ui.email.EmailActivity;

public class EmailProvider implements Provider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are deleting a formerly public class. Should this have always been annotated with @RestrictTo or this is a real breaking change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment for all providers.

Copy link
Collaborator Author

@SUPERCILEX SUPERCILEX Mar 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was from before we used the restrict annotations, but it's always been considered private API AFAIK:

/**
* IDP-specific interactions for signing in users. The contents of this package should be considered
* an implementation detail and not part of the main API.
*/

Also, we just broke people so these should really be marked as restricted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM let's just mark them all as restricted now

@SUPERCILEX SUPERCILEX changed the title [WIP] Completely rewrite provider login with new architecture Completely rewrite provider login with new architecture Mar 17, 2018
@SUPERCILEX
Copy link
Collaborator Author

@samtstern Alrighty! Hope you had a great weekend, this PR is officially no longer a WIP. 👏 I've tidied a bunch of stuff up, finished implementing everything that needed to get done, and thoroughly tested everything until all those 🐛 were 💥 into oblivion.

The next PR is only a few hundred lines of code so once we get passed this, we'll be on a roll! 🥂 (My poor attempt at a motivational speech and apology for making this PR so big. 😆)

@SUPERCILEX
Copy link
Collaborator Author

Thanks for the great feedback, I don't mind at all! 😊 I'll address your flow questions here. Things should hopefully seem much clearer now:

  • ProviderBase holds the context related stuff plus a LiveData with an IdpResponse so people can subscribe to "provider responses."
  • ProviderResponseHandler takes in an IdpResponse and converts it to a Firebase Auth sign-in, then simply flushes the response through (though that can certainly change if need be in the future).
  • ProviderHandlerBase is in charge of getting a response for a specific provider implementation from their backend.

I'd recommend looking at these classes from a package perspective:

  • ui -> ProviderBase
  • viewmodel -> ProviderResponseHandler
  • remote -> ProviderHandlerBase

Once everything clicks, I think you'll appreciate the clean separation we have rockin! 😁

@@ -357,22 +359,12 @@ public static int getDefaultTheme() {
}

private Task<Void> signOutIdps(@NonNull Context context) {
try {
if (FacebookSignInHandler.IS_AVAILABLE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much less scary

@@ -64,20 +80,16 @@ public State getState() {

@Nullable
public final Exception getException() {
mUsed = true;
if (mUsable) { mState = State.USED; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sort of interesting. Previously, a Resource was immutable. You'd listen to a stream and get a new resource every time the state changed. Now the state can change from the inside.

How does this affect the pattern of listening to LiveData<Resource> and checking state? Is it a problem that Observers won't get a new callback when this happens?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, technically, the "resource" part is still immutable since the value and exception can't change. Also, the class is final so only we control the state's mutability, and for now, it can only go from SUCCESS/FAILURE -> USED so I don't think it'll be a problem unless we want to listen for USED events? I'm not sure when that would ever be applicable... I think of the state as more for helping us define our conditionals rather than defining the resource—it should be focused on the data, not the state. That's my opinion though, any digressions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems OK for now ... just something we may have to change in the future.


@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class ProviderBase implements Provider {
private final ProviderResponseHandlerBase mHandler;
public abstract class ProviderBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your github comments explaining the class hierarchy were very helpful! Let's also add class-level JavaDoc comments to these new base/handler classes to explain it to future me when I inevitably confuse myself again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, yep!

setResult(Resource.<IdpResponse>forFailure(e));
public Task<Void> then(@NonNull Task<AuthResult> task) {
if (mRequestedSignInCredential == null) {
return Tasks.forResult(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment here explaining why in this case we don't care at all if the signInWithCredential operation was successful or not?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, that was a mistake, thanks for catching it!

.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the way you have this set up the task is guaranteed to be successful (in the if branch you go straight to success and in the else branch you drop errors). Is that your intention?

I think this class would be improved with a healthy dose of comments since I am a little lost in what you're trying to achieve and why some error cases are ignored.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, my B.

@@ -47,7 +47,6 @@
<string name="fui_error_unknown">發生不明錯誤。</string>
<string name="fui_error_invalid_password">密碼不正確。</string>
<string name="fui_accessibility_logo">應用程式標誌</string>
<string name="fui_signed_in_with_specific_account">以 %1$s 的身分登入</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reverting the tools declaration, but can we just revert all changes to the strings files? Is there some lint about an unused resource you're trying to get around?

I would much rather ignore the lint for now than have all of these strings changes cluttering up this very important PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh gotya, I'll tell lint to keep calm and let the resources be unused. 😆

return false;
}

protected abstract void signIn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems duplicative to have both this method and startSignIn? Couldn't this method just check the state and set the loading state in the base class and be annotated with @CallSuper?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've looked at this again and stuff has changed so whatever I was doing is now uncessessary. I'm going to simplify it and remove the abstractions.

import com.firebase.ui.auth.ui.HelperActivityBase;

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class ProviderBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this class is only abstract methods ... could it be an interface instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I didn't notice!

}

@Override
public void startLogin(@NonNull HelperActivityBase activity) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. nit, but for consistency this should probably be called startSignIn since we use the term 'sign in' not 'log in' pretty much everywhere.

  2. I find it a bit strange that this class talks directly to LoginManager. Shouldn't all of the Facebook SDK code be contained within the FacebookSignInHandler and this class merely acts as a pass-through?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, fixed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the second part, I'm not doing that so we don't send the activity over to the view model... do you think we should?

@samtstern
Copy link
Contributor

Ok I have spent an hour staring at things and I think we can beat a little more sense into the class hierarchy still. Here are the bits that still seem somewhat off to me.

ProviderBase

  • This class does a few somewhat unrelated things. The first thing it does is serve as a holder for a StringRes and a LayoutRes. These are constants per-provider. We could move these out into a separate class called ProviderView and store them in a simple static map somewhere from String --> ProviderView so you could just do map.get(PROVIDER_ID).getNameRes() and be done with it.

  • The second thing this class does is hold a ProviderHandlerBase, at least for IDPs. The fact that the class holds one of these is not evident from reading the ProviderBase class definition (small problem in itself) but in practice, that's what it does. It seems that the startLogin, onActivityResult and getResponseListener methods are little more than dumb proxies for the same methods in the ProviderHandlerBase.

  • The two classes that act differently here are EmailProvider and PhoneProvider, since rather than talking to a Handler they kick off some other activity and wait for the response. So maybe this is the justification for the whole abstraction?

ProviderHandlerBase

  • This is only used for social providers (Google, Facebook, etc) whereas Phone and Email are handled locally. However this is not clear from the class hierarchy because ProviderBase does not make any distinction and the class names don't either.

  • Could the phone and email logic be pushed into a ProviderHandlerBase? Or would it not fit that model?

ProviderResponseHandlerBase

  • This is basically an AuthViewModel with an extra sign in method.
  • Has only two subclasses, SimpleProviderResponseHandler and LinkingProviderResponseHandler.
  • There's only one place where we use the base class as either an argument or a return type (in `AuthMethodPickerActivity.populateIdpList)
  • I'm wondering if we can just remove the base layer of abstraction here at the expense of an extra if block in the auth method picker?

@SUPERCILEX
Copy link
Collaborator Author

Thanks for the really thoughtful feedback!

ProviderBase
TBH, this was just around because it was there before. I've looked at where some of the methods are being used and just inlined stuff. I've also moved the sign-in start process to the handler with a HUGE warning not to use the activity outside of that method.

ProviderHandlerBase has been squishified into ProviderBase.

ProviderResponseHandlerBase Yup, I did that in an earlier commit.

Ok, what do you think of https://github.com/SUPERCILEX/FirebaseUI-Android/commit/e9470309dbfb68caf0b45216afed3f13d6abe17b? I think we've finally reached critical mass and there's nothing left to remove. 😆

Signed-off-by: Alex Saveau <[email protected]>
@samtstern
Copy link
Contributor

@SUPERCILEX wooo! I have to head out a bit early today but on first pass 👍

I think this is really clear now, and also more lines deleted than added! Not bad for a refactor that makes everything work better.

Signed-off-by: Alex Saveau <[email protected]>
@SUPERCILEX
Copy link
Collaborator Author

Alrighty, SGTM! Thanks!

Copy link
Contributor

@samtstern samtstern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L.
G.
T.
M.

💥 🎉 🌮 💸

@samtstern
Copy link
Contributor

@SUPERCILEX thanks for all the back and forth, all of this code is really clear now (or as clear as the tangled auth mess that is the AuthUI flow will ever be!).

I am going to merge this PR now, if you had any TODOs we can add them to your next one.

Congrats on making this happen, get yourself a beer (or two, or orange juice, or whatever, do you).

@SUPERCILEX
Copy link
Collaborator Author

YASSSSSSSS! Stellar use of emojis BTW. 👌😆 You enjoy a beverage too, all of that reviewing must turn your brain into mush! 😳😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants