Skip to content

Request Twitter email #452

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 12 commits into from
Dec 15, 2016
Merged

Request Twitter email #452

merged 12 commits into from
Dec 15, 2016

Conversation

SUPERCILEX
Copy link
Collaborator

private final String mProviderId;
@Nullable
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 don't think this should be nullable: we always know the email address and provider id and since having it be nullable caused a crash, I made it @nonnull.


@Override
public void failure(TwitterException exception) {
TwitterProvider.this.failure(exception);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a problem:

Even if the user grants access to her email address, it is not guaranteed you will get an email address. For example, if someone signed up for Twitter with a phone number instead of an email address, the email field may be empty. When this happens, the failure method will be called because there is no email address available.

It will be confusing to the user if they press authorize and the sign in just fails unexpectedly because they didn't give Twitter an email. The only place we need to display a message is in AuthMethodPickerActivity, but what should we say and how? A snackbar? An alert dialog?

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 a problem ... I need to look into this and ask someone who knows more than I do :-)

@SUPERCILEX
Copy link
Collaborator Author

@samtstern The twitter tests are failing because we are waiting for a callback that will never come (the ok from the user in twitter's request email activity). Because of that, I don't think it's possible to test the TwitterProvider, but it also doesn't look like we really need the tests: we manually call twitterProvider.success(result); and don't check anything after that:

@Override
public void onSuccess(IdpResponse idpResponse) {
    assertTrue(mAssertSuccess);
    mCountDownLatch.countDown();
}

Is it ok for me to remove the test? Or how could I get it to work with requestEmail?

Here's the test in question: https://github.com/firebase/FirebaseUI-Android/blob/master/auth/src/test/java/com/firebase/ui/auth/ui/provider/TwitterProviderTest.java#L68

@@ -60,8 +74,36 @@ public void startLogin(Activity activity) {
}

@Override
public void success(Result<TwitterSession> result) {
mCallbackObject.onSuccess(createIdpResponse(result.data));
public void success(final Result<TwitterSession> sessionResult) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Twitter caused a leak by storing our callback in a result receiver:

In com.firebase.uidemo:1.0:1.
* com.firebase.ui.auth.ui.idp.AuthMethodPickerActivity has leaked:
* GC ROOT android.os.ResultReceiver$MyResultReceiver.this$0
* references com.twitter.sdk.android.core.identity.ShareEmailResultReceiver.callback
* references com.firebase.ui.auth.provider.TwitterProvider$EmailCallback.this$0
* references com.firebase.ui.auth.provider.TwitterProvider.mCallbackObject
* leaks com.firebase.ui.auth.ui.idp.AuthMethodPickerActivity instance
* Retaining: 11 KB.
* Reference Key: 1437fccd-e7e7-4aa3-9154-b8421f639999
* Device: Google google Pixel XL marlin
* Android Version: 7.1 API: 25 LeakCanary: 1.5 00f37f5
* Durations: watch=5020ms, gc=196ms, heap dump=14858ms, analysis=156016ms

and their RequestEmail activity was bothering me because it's useless: devs already have access to the email since they requested it in the Twitter console. Sigh to twitter 😄

Anyway, I dug through the source code and ripped Twitter's GET request so that we can handle that stuff ourselves. Now if we didn't get an email, we know whether or not the dev hasn't requested the email permission in the twitter dashboard or if the Twitter user just doesn't have an email.

@samtstern Since we now have control over what's going on, we can decide what to do in the two failure cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

@SUPERCILEX I appreciate all the investigation but I have a few reservations about this approach:

  • We are supposed to be implementing a best-practices UI layer on top of various auth SDKs, but this is clearly not what Twitter recommends as best practice.
  • This adds a lot of complexity by introducing Retrofit into our code (I know it was a dependency before, but now we are using it directly)
  • The benefit here is real, but it's not huge. I think I'd rather use the Twitter SDK as it stands and just deal with the ambiguity of their errors.

As for the leak, I think we can work around that ourselves by making the callback a static inner class with a WeakReference to the Activity.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@samtstern Whoa, you just blew my mind! The main reason I didn't want to use requestEmail was because of the leak, but now that you showed me how to use WeakReferences... Wow! This is why I love contributing to FirebaseUI so much: I've read a few Medium posts on weak references, but I need people like you to show me where I can apply the concepts. Anyway, thanks for the tip! (I had to go back and rewrite some code in my app to use weak references 😄)

@@ -50,6 +50,9 @@ dependencies {
// library when updating com.android.support libraries:
compile "com.android.support:cardview-v7:$support_library_version"

compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No worries about these, Twitter's already includes them and I'm just upgrading it for them:

+--- com.twitter.sdk.android:twitter:2.3.0
|    +--- com.twitter.sdk.android:tweet-composer:2.3.0
|    |    +--- com.twitter:twitter-text:1.13.0
|    |    +--- io.fabric.sdk.android:fabric:1.3.14
|    |    +--- com.twitter.sdk.android:twitter-core:2.3.0
|    |    |    +--- com.google.code.gson:gson:2.6.1 -> 2.7
|    |    |    +--- com.squareup.retrofit2:converter-gson:2.0.2 -> 2.1.0 <<<<<<<<<<<<
|    |    |    |    +--- com.squareup.retrofit2:retrofit:2.1.0
|    |    |    |    |    \--- com.squareup.okhttp3:okhttp:3.3.0
|    |    |    |    |         \--- com.squareup.okio:okio:1.8.0
|    |    |    |    \--- com.google.code.gson:gson:2.7
|    |    |    +--- com.squareup.okhttp3:okhttp:3.2.0 -> 3.3.0 (*)
|    |    |    +--- io.fabric.sdk.android:fabric:1.3.14
|    |    |    \--- com.squareup.retrofit2:retrofit:2.0.2 -> 2.1.0 (*)
|    |    \--- com.squareup.picasso:picasso:2.5.2
|    +--- io.fabric.sdk.android:fabric:1.3.14
|    +--- com.twitter.sdk.android:twitter-core:2.3.0 (*)
|    \--- com.twitter.sdk.android:tweet-ui:2.3.0
|         +--- io.fabric.sdk.android:fabric:1.3.14
|         +--- com.twitter.sdk.android:twitter-core:2.3.0 (*)
|         +--- com.android.support:support-v4:23.1.1 -> 25.0.1 (*)
|         \--- com.squareup.picasso:picasso:2.5.2


@Override
public void success(Result<String> emailResult) {
mCallbackObject.get().onSuccess(createIdpResponse(emailResult.data));
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to check if mCallbackObject.get() is null here before calling onSuccess to avoid a NPE.

When it's null I guess we can just log a warning and do nothing, it means the activity waiting for the callback was dead so there's nowhere to deliver 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.

@samtstern so does this mean that on a low memory device onSuccess won't be called?

Log.e(TAG, "Failure retrieving Twitter email. " + exception.getMessage());
// If retrieving the email fails, we should still be able to sign in, but Smart Lock
// and account linking won't work.
mCallbackObject.get().onSuccess(createIdpResponse(null));
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here.

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

OK I just checked out this PR at the latest commit and tried a number of situations:

  • Twitter account that has email

    • When collision, it linked properly
    • When I signed in and clicked "deny" on the Twitter email screen FirebaseUI still seemed to get my email. Deny and allow behaved the same for this account.
  • Twitter account that only has phone number

    • The allow/deny email screen still showed up
    • No matter which one I clicked, I obviously could not get an email from Twitter

So now I am wondering: when does this request email screen ever do anything? What sort of account setup would result in Firebase Auth only getting the email when that screen is presented.

@SUPERCILEX
Copy link
Collaborator Author

When I signed in and clicked "deny" on the Twitter email screen FirebaseUI still seemed to get my email. Deny and allow behaved the same for this account.

@samtstern do you mean the welcome back screen still showed up? The account linking/welcome back screen isn't showing up for me when I press deny on Twitter's email screen. If account linking didn't occur, but FirebaseUser#getEmail() still works, that's expected behavior because firebase auth is getting the Twitter email some other way.

The allow/deny email screen still shows up if the twitter user doesn't have an email

This is working according to Twitter's documentation: we have to show the email screen to know if the Twitter user has an email.

What sort of account setup would result in Firebase Auth only getting the email when that screen is presented.

That was one of the reasons I chose to use the retrofit hack, we can't. Firebase auth doesn't care what we do because they get the Twitter email some other way.

@samtstern Do you want to proceed with this PR or go back to the retrofit hack? Twitter does want us to use their request email activity, but it seems redundant in most cases because Firebase Auth gets the email anyway.

@samtstern
Copy link
Contributor

@SUPERCILEX sorry maybe I am confused but I think my question is why do we need either the retrofit hack or the request email screen? It seems to me that Firebase Auth is already going to get us the email address whenever possible and all we need to do in FirebaseUI is handle the case where twitter email is null (which you've done)

@SUPERCILEX
Copy link
Collaborator Author

@samtstern Now I'm all confused too! 😄 From what I understand, we need the email in IdpResponse because of this code in CredentialSignInHandler:

if (!task.isSuccessful() && task.getException() instanceof FirebaseAuthUserCollisionException) {
    // At this point, the user isn't signed in right?
    mHelper.getFirebaseAuth()
            // If the user isn't signed in, this will always fail whether or not the Twitter user has an email because we can't use FirebaseUser#getEmail() yet.
            .fetchProvidersForEmail(mIdpResponse.getEmail())
            // ...
}

@samtstern For account linking, we need the email from IdpResponse before the user is logged in, right?

@samtstern
Copy link
Contributor

@SUPERCILEX I think the account linking cases is not relevant here, right? Would our flow ever initiate account linking for an account with no email?

@SUPERCILEX
Copy link
Collaborator Author

@samtstern Well no, but this PR was originally created to handle the case where a Twitter user with an email tries to link their account: user creates email account -> user tries to sign into Twitter with the same email they created their email account with -> we need the email before the user is signed in to perform account linking.

@samtstern
Copy link
Contributor

I finally get it! Your last comment turned on the lightbulb for me. I'm going to summarize below just in case I forget again:

  1. User signs up with email [email protected] using either email/pw or some non-Twitter IDP
  2. User tries to sign up with a Twitter account with that email.
  3. We acquire a Twitter auth credential. This does not have the email address so we can't check for providers.
  4. We launch into the sign in flow and get a FirebaseAuthUserCollisionException because the Firebase auth backend used our Twitter token to get the email address and saw a collision that we didn't see.

But with your change we have:

3a. Use the twitter SDK to request the email and check for collisions, launching the welcome back flow if necessary.

Now that I finally understand the edge case here I think the way you are doing it now (launching the twitter email screen) is the correct approach.

@SUPERCILEX
Copy link
Collaborator Author

@samtstern do you want to wait for your email refactor to go in before merging this PR or are there a few changes that still need to be addressed before merging?

@samtstern
Copy link
Contributor

Nope this LGTM!

@samtstern samtstern merged commit c52387e into firebase:version-1.1.0-dev Dec 15, 2016
@SUPERCILEX SUPERCILEX deleted the twitter branch December 15, 2016 23:36
@SUPERCILEX
Copy link
Collaborator Author

Awesome!

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