firebase / FirebaseUI-Android

Optimized UI components for Firebase
https://firebaseopensource.com/projects/firebase/firebaseui-android/
Apache License 2.0
4.63k stars 1.84k forks source link

Anonymous user and account conversion to permanent account #123

Closed danielkummer closed 4 years ago

danielkummer commented 8 years ago

Hi there

Is there a way in which AuthUI supports the conversion of a anonymous account to a permanent one (one of the supplied auth providers) As stated in the documentation and as I understand it, one must use the linkWithCredential method instead of the "normal" signInWith flow.

Can this be done with the current AuthUI version somehow?

iainmcgin commented 8 years ago

Hi Daniel,

We don't have any direct support in FirebaseUI for upgrading anonymous accounts to an account backed by an authentication method just yet. You can do it directly with the Firebase Auth APIs, see here in the section "Convert an anonymous account to a permanent account".

I'll leave this issue open as a feature enhancement request; we can likely handle this automatically within FirebaseUI if an existing anonymous user is present in auth.getCurrentUser() when sign in occurs during the authentication flow.

danielkummer commented 8 years ago

Hi @iainmcgin - thanks for your reply (and considering it as a feature enhancement :) )

I've read through the documentation you mentioned - maybe you can answer me this: Is there a way to get the AuthCredential object from AuthUI which was used by the user to sign-in? As I'm reading the docs, without this object instantiated with the users credentials there's not way to link accounts...

iainmcgin commented 8 years ago

Ah, we have a slight problem here after discussing this with some colleagues. There is deliberately no way to get back an AuthCredential from an existing user, to avoid potentially leaking credentials. So, the linking of the authentication method to the anonymous account would have to occur within Firebase UI if you are using the library. I'll take a look at this next week to get you unblocked ASAP.

danielkummer commented 8 years ago

Great - looking forward to hearing from you πŸ‘ (And thanks a lot!)

guillermomuntaner commented 8 years ago

Facing similar case. My app uses default anonymous login to provide a frictionless experience while being able to store user data. Log-in (non anonymously) is a secondary optional feature if the user wants to save his data between devices or access specific features. So my app doesn't really have a true logout status and instead rely on anon/non-anon statuses.

1 - Simple scenario; An anonymous user with some local data decides to sign up with X provider (new account). In this case FirebaseUI creates a new user instead of linking the new credentials to the current anonymous user. Moreover the new user is loged in without a warning, so the anon user is lost for ever. ΒΏWouldn't be a good default behaviour to try to link the credentials first? In case an account for those credentials already exists linkWithCredential will return an exception which can be handled as follow (see scenario 2)

2 - Complex scenario: An anonymous user tries to sign in (to an existing account). In this scenario we have 2 users and I guess it should be managed by the developer. So some sort of callback/result.

I see how this is problematic due to the potential complexity of second case, but at least the 1st case can be solved very easily by trying to link 1st new credentials when an anonymous user is in.

drewhannay commented 8 years ago

@iainmcgin Any updates on this? I'm blocked on the same issue and wondering if I will need to fork FirebaseUI to proceed...

pcarbonn commented 8 years ago

Same issue here ! Waiting for a fix... In fact, I'm using firebase oauth for a web app, so I'll post in the relevant github project.

janakagamini commented 8 years ago

I'm quite keen on this feature too.

pantos27 commented 8 years ago

Looking forward for this enhancement on both Android and iOS

shalama commented 8 years ago

Has this feature been implemented in the 0.5.1 release? I'm also really looking forward for this.

samtstern commented 8 years ago

@shalama this has not been implemented yet. It's something we are looking into for future releases though.

You can see exactly what changes with each release by visiting the release notes: https://github.com/firebase/FirebaseUI-Android/releases

GaborPeto commented 7 years ago

+1 for the feature request

bobshao commented 7 years ago

Hi @iainmcgin , we have the same feature requirement. Does team have any workaround for this? Hope you can have a response :)

SUPERCILEX commented 7 years ago

@bobshao Unfortunately, there aren't any workarounds as mentioned above, but I'm going to be able to work on #309 again so I'm hoping for it to cut FirebaseUI v1.1.0 which will be coming out soon.

bobshao commented 7 years ago

Thanks very much for your quick response :) @SUPERCILEX

curiousily commented 7 years ago

It looks like 1.1.0 is shaping up at #510. Will #309 be included in the version?

samtstern commented 7 years ago

@curiousily this will not be making it into the 1.1.0 release due to cross-platform issues. We don't have an implementation on iOS yet.

curiousily commented 7 years ago

@samtstern thanks for the info. Will love to hear any plans concerning when this will be merged.

percula commented 7 years ago

Any progress on this? This feature would be great for an app I'm working on.

SUPERCILEX commented 7 years ago

@percula #309 is ready to be merged, but it's waiting on https://github.com/firebase/FirebaseUI-iOS/issues/139.

percula commented 7 years ago

Thanks @SUPERCILEX ! Looks like there's good activity on the iOS side now.

Beginner question: I want to use your implementation of Anonymous Auth linking, so I cloned your fork. Then I followed the instructions to install the repository to Maven Local ./gradlew :library:prepareArtifacts :library:publishAllToMavenLocal. Now, how do I add your fork as a dependency in Gradle and make sure that I'm getting the fork and not the master FirebaseUI? I currently have: compile 'com.firebaseui:firebase-ui-auth:1.1.1', but don't think that's right.

samtstern commented 7 years ago

@percula if you change the version number in constants.gradle and then install locally again you can be sure you have the local version.

SUPERCILEX commented 7 years ago

@percula @samtstern Whoa whoa whoa, you guys aren't being lazy enough! πŸ˜„ I would just use JitPack if I were you. Once you follow step one on their website (add the maven reop), you can just add my fork like so:

compile 'com.github.SUPERCILEX.FirebaseUI-Android:firebase-ui-auth:d1df8d2c0aef03f3db008d4021059ba316386c7c'

That's way easier to start and to keep yourself up to date with my fork. Cheers! πŸŽ‰

samtstern commented 7 years ago

@SUPERCILEX I can't believe Jitpack works with our crazy build system. Very impressive.

SUPERCILEX commented 7 years ago

@samtstern Yeah, it took a while for me to figure out, but it would have been too much of a pain to re-compile everything every time I wanted to test it in my own app. Basically because we have several modules, instead of com.github.SUPERCILEX:FirebaseUI-Android:version we have to use com.github.SUPERCILEX.FirebaseUI-Android:firebase-ui:version.

percula commented 7 years ago

@SUPERCILEX Thanks! Works like a charm

zzsdeo commented 7 years ago

Hello! I have try to add dependency as shown below compile 'com.github.SUPERCILEX.FirebaseUI-Android:firebase-ui-auth:3a8a53dd5a9709a5cdd6c1e721f0adf07d18478e' but it conflicts with another dependency compile 'com.google.android.gms:play-services-gcm:10.2.0' What dependency I must add to avoid conflicting with play services 10.2.0?

SUPERCILEX commented 7 years ago

@zzsdeo Here's the updated commit hash with the new dependencies:

compile 'com.github.SUPERCILEX.FirebaseUI-Android:firebase-ui-auth:d1df8d2c0aef03f3db008d4021059ba316386c7c'
zzsdeo commented 7 years ago

@SUPERCILEX thanks a lot!

zzsdeo commented 7 years ago

@SUPERCILEX can you give the commit hash with account linking support in version 2.0.1

percula commented 7 years ago

Just wondering, how do you generate the commit hash? Thanks!

pamartineza commented 7 years ago

If we can't present as an option an "anonymous" or "skip login" option using FirebaseUI library why is not anonymous account linking enabled?

As far as I can understand #309 PR code is ready and approved but not merged due to an "ios feature parity" requirement which IMHO is nonsense, whether to use a feature available or not in various platforms should be a technical decision taken by us as developers or CTO's.

Despite of that could you please @SUPERCILEX clarify if anonymous account linking is already implemented in 2.0.1 version? if not, from which alternative jitpack repo is it available? (if it is)

Thanks in advance

SUPERCILEX commented 7 years ago

@zzsdeo @percula @pamartineza I haven't had time to support phone auth yet so you can use it, but there won't be any linking going on unfortunately, sorry. I have my TODO list for auth (https://github.com/SUPERCILEX/Robot-Scouter/issues/136), but I'll also try to remember to update this issue whenever I have the time to get linking working properly with phone auth. As for the 2.0 commit hash, here you go:

implementation "com.github.SUPERCILEX.FirebaseUI-Android:firebase-ui-auth:ce8478d641"
samtstern commented 7 years ago

@pamartineza just to address one additional point you made, I don't think we'd ever add an "Anonymous" option to FirebaseUI in the auth method picker screen. Anonymous authentication should happen without user interaction.

SUPERCILEX commented 7 years ago

Oh yeah, forgot to mention that. Thanks for the clarification @samtstern

@pamartineza The ideal flow IMO looks something like this:

  1. On app boot if the user isn't signed in, sign them in anonymously
  2. Show them some tutorial where they started creating content immediately to experience the worth of your app
  3. At some point, show them a message like "Wassup bro, wanna create an account to save your data?" πŸ˜† That's where you use FirebaseUI to link the anonymous account data to the one they create in the sign-in flow.

Hope that clears things up even further! πŸ˜„

pamartineza commented 7 years ago

Thanks for your prompt answers, @samtstern , @SUPERCILEX

@SUPERCILEX this is also our ideal flow, but it is paradoxical that official answer is, "yes, you have to sign in anonymously your users on your own, we won't support that option in FirebaseUI, but if you do that, you won't be able to use FirebaseUI because we don't support anonymous account linking"

I have addressed this issue many times to Firebase representatives at Google Campus in Madrid (Spain) since Firebase was acquired by Google and just last June 21th in a Firebase event they assured me that this was already solved, now I'm again confused to see that it is not...

pamartineza commented 7 years ago

@SUPERCILEX I have successfully linked anonymous accounts to Gmail accounts using your fork but photoUrl and displayName are null and empty respectively, is this an expected behaviour or an issue related with #729 ?

SUPERCILEX commented 7 years ago

Aha! @pamartineza Thanks for help me figure it out, yeah it does relate to that issue: https://github.com/firebase/FirebaseUI-Android/issues/729#issuecomment-310677279.

drod744 commented 7 years ago

Hello, is there an update on when we can expect this to be released? This has been open for over a year now. It would be cool to at least have some realistic expectation of when it will be released. Thanks.

SUPERCILEX commented 7 years ago

@pamartineza Aw crud! 😒 Turns out the null photo and name is expected behavior according to the support agent I contacted:

Hi Alexandre,

Hope you're having a great day. I appreciate your time and effort on voicing this out.

I have talked to our engineer and he confirmed that this is an intended behavior. A good explanation for that is, if the user_id already exists in Firebase Auth, user attributes from Identity Provider (Google, Facebook etc.) are not populated as top level attributes. Instead, they are updated in sub-level user.providerUserInfo.

Let me know if you have any other Firebase-related issues/questions. Thank you for using Firebase and I wish you all the best in your project. :)

Cheers, Hazel

I'll have to update my PR to do some sort of merge based profile update i.e. if the previous user's name is null but the new one isn't, update the name and etc. for the profile photo. So anyway, I've added it to my growing TODO list.

LeChatNoir69 commented 7 years ago

Hi, Looking for the same behavior. I think we want to use firebase database in any cases: before user logged in and after too. I thought about one solution : as soon as user tap on signIn button, before launching firebaseUI, save datas in memory. Then after signIn, if succeed, fetch memory in firebase new user DB space. But your fork seems nicer solution.

pamartineza commented 7 years ago

@SUPERCILEX

what we are doing in our current implementation is getting them on the activity on result and then after linking successfully the Gmail account requesting a ProfileUpdate

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

        if (requestCode == RC_SIGN_IN) {

            val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)

            if (result.isSuccess && result.signInAccount != null) {
                Logx.d("successful Google sign in " + result.status.statusMessage + " " + result.signInAccount?.idToken + " " + result.signInAccount?.photoUrl)

                mainPresenter.onUserSelectedGmailAccount(result.signInAccount?.idToken as String, result.signInAccount?.photoUrl.toString(), result.signInAccount?.displayName)

            } else {
                Logx.d("failed Google sign in " + result.status.statusMessage)
                mainPresenter.onGoogleSignInFailed()
            }

        } else {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }
fun updateProfileWithDisplayNameAndPhotoUrlCompletable(displayName: String?, photoUrl: String?): Completable {

        return Completable.create { emitter ->

            val builder = UserProfileChangeRequest.Builder()

            if (null != photoUrl) {
                builder.setPhotoUri(Uri.parse(photoUrl))
            }

            builder.setDisplayName(displayName)

            val currentUser = auth.currentUser

            if (null != currentUser) {

                currentUser.updateProfile(builder.build()).addOnCompleteListener { task ->

                    if (task.isSuccessful) {
                        Logx.d("profile updated successfully")
                        emitter.onComplete()
                    } else {
                        Logx.e("profile update failed", task.exception)
                        val exception = task.exception

                        if (null != exception) {
                            emitter.onError(exception)
                        } else {
                            emitter.onError(UnknownError("Unknown update profile error"))
                        }
                    }
                }
            } else {
                emitter.onError(IllegalStateException("currentUser is null"))
            }

        }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())

    }
drod744 commented 7 years ago

Hello again, is there an update on when we can expect this to be released? What is the current status? Is this the wrong place to ask this question? If not, can someone point me somewhere where I can get a response? thanks.

samtstern commented 7 years ago

@drod744 we don't offer release timelines, if you read the thread you'll see this will only be released when there is a similar feature on FirebaseUI-iOS.

You can use it now by checking out the branch and building the library locally.

SUPERCILEX commented 7 years ago

Hello everyone, I have a new release ready to share with you guys! πŸ˜„πŸš€

Some highlights:

Add the following to your build.gradle file to get this release:

implementation "com.github.SUPERCILEX.FirebaseUI-Android:firebase-ui-auth:164165ed92"
harvitronix commented 7 years ago

@SUPERCILEX Thanks for all the hard work on this. The flow that you've described (automatically logging in anonymously, merging data after logging in) is exactly what I'm looking for.

Can you explain what I need to do to enable "automatic profile merging" so that the logged in user can access their data from the anon state? The word "automatic" implies it should just work once I swap out the standard FirebaseUI with yours in build.gradle, but I think I'm missing something.

My flow is:

Thanks again.

SUPERCILEX commented 7 years ago

@harvitronix Glad to hear this PR is useful! πŸ˜„

You've almost got it, but you're confusing a few things. The "automatic profile merging" I was talking about is literally the user profile so FirebaseUser#getName() and FirebaseUser#getPhotoUrl(). That all happens automatically meaning when the user signs in with a real account (e.g. Google) and their name or photoUrl is null, we add the login profile metadata to their account. You can see the merging in action in ProfileMerger.java.

What you're looking for is database merging and some pretty extensive work has gone into getting that right. However, you are correct in that we can't automagically do all the merging for you. In most cases, it Just Worksℒ️ and we can simply upgrade the account type, but if a few rare cases you'll have to manually perform a database merge. I've got some detailed docs that should help you with that: https://github.com/SUPERCILEX/FirebaseUI-Android/tree/master/auth/README.md#handling-account-link-failures.

Just in case this wasn't clear, you do have to manually set a builder flag to enable account linking because of the aforementioned caveat: scroll to the bottom of the sign-in examples or just above handling responses.

Feel free to ask me any more questions! πŸ˜„

harvitronix commented 7 years ago

@SUPERCILEX Ah ha! Thank you for clarifying for me. I added setIsAccountLinkingEnabled(true) to the builder and voila, works like a charm. Can't thank you enough for all the work you've done!

How would you handle this scenario:

Since the second time is not a new account creation, there's no merge, and so the user no longer has access to their anonymously-created data in the DB. I tried to use the Handling account link failures example as my guide, but when I attempt to get the data the user collected while anonymous, the DB gives me a permission denied.

My DB rules are...

...
"$uid": {
    ".read": "auth.uid === $uid",
    ".write": "auth.uid === $uid",
}
...

And my structure is like...

"users": {
    "uid1": {...},
    "uid2": {...},
}
SUPERCILEX commented 7 years ago

@harvitronix Sorry for the late reply, I didn't see that you edited your comment! πŸ˜•

New response:

While writing my response, you gave my a brilliant idea! πŸ˜„ I found a bunch of bugs in Google's new Play Billing library most of which stem from various memory leaks because they're storing a listener that can include a context:

// Create a result receiver that will propagate the result from InvisibleActivity
// into PurchasesUpdatedListener specified in the constructor.
ResultReceiver purchaseResultReceiver =
  new ResultReceiver(mUiThreadHandler) {
    @Override
    protected void onReceiveResult(int responseCode, Bundle resultData) {
      List<Purchase> purchases =
          (resultData == null) ? null : BillingHelper.extractPurchases(resultData);
      mBroadcastManager.getListener().onPurchasesUpdated(responseCode, purchases);
    }
  };
// Launching an invisible activity that will handle the purchase result
Intent intent = new Intent(activity, ProxyBillingActivity.class);
intent.putExtra(RECEIVER_EXTRA, purchaseResultReceiver);
intent.putExtra(RESPONSE_BUY_INTENT, buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT));
// We need an activity reference here to avoid using FLAG_ACTIVITY_NEW_TASK.
// But we don't want to keep a reference to it inside the field to avoid memory leaks.
// Plus all the other methods need just a Context reference, so could be used from the
// Service or Application.
activity.startActivity(intent);

Anyway, that made me realize that you can pass around callbacks inside intents! I don't have time to work on this right now, but basically this will let me pass around a callback to tell the dev when they should transfer their data and then finish the sign-in. There shouldn't be any memory leaks because you shouldn't need an Activity specific context to transfer your user data.

TL;DR: I'm going to work on a fix sometime next week that will hopefully solve your problem! πŸ˜„

Old response:

What I really wanted with this PR was a way to send a callback in that no man's land state between the old account and the new one. However, that kind of refactor would be an in incredible pain in the rear so I haven't done it. Instead, I have two ideas that could work.

  1. This one wastes a bunch of data and time depending on how your db is organized. Basically, anytime the user is preparing to sign in you download all the data you need to transfer and keep it in memory somewhere and then use it if there's a prevUid.
  2. I have another less sucky solution, but it decreases the safety of your user data for a brief period of time. Basically anytime you start a login you set a field like isSigningIn to true and your read rule becomes ".read": "auth.uid === $uid || data.child("isSigningIn").val() === true. For the sign-in duration, anyone could access the data, but it makes your life easier.
SUPERCILEX commented 7 years ago

@harvitronix Ok, I have some good news and some bad news. The bad stuff first: Turns out if your process dies the ResultReceiver also dies and I loose your code which kills that idea.

The good news! I took things to an extreme and we now have a custom service that handles data transfer. The issue with adding a service is that it significantly increases complexity, but at least it works.

I'm going to do some more testing and thinking about how I can improve my code (because it's gross) so I'll probably release the new stuff later this week.

For now, here are the security rules I'm testing with:

{
  "rules": {
    ".read": "false",
    ".write": "false",
    "chatIndices": {
      "$uid": {
        ".read": "auth.uid === $uid",
        ".write": "auth.uid === $uid"
      }
    },
    "chats": {
       "$key": {
          ".read": "root.child('chatIndices').child(auth.uid).child($key).exists()",
          ".write": "root.child('chatIndices').child(auth.uid).child($key).exists()"
       }
    }
  }
}

And here's the transfer code:

public class MergerService extends ManualMergeService {
    private Iterable<DataSnapshot> mChatKeys;

    @Override
    public Task<Void> onLoadData() {
        final TaskCompletionSource<Void> loadTask = new TaskCompletionSource<>();
        FirebaseDatabase.getInstance()
                .getReference()
                .child("chatIndices")
                .child(FirebaseAuth.getInstance().getCurrentUser().getUid())
                .addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot snapshot) {
                        mChatKeys = snapshot.getChildren();
                        loadTask.setResult(null);
                    }

                    @Override
                    public void onCancelled(DatabaseError error) {
                        Log.e("TAG", "message", error.toException());
                    }
                });
        return loadTask.getTask();
    }

    @Override
    public Task<Void> onTransferData(IdpResponse response) {
        String uid = FirebaseAuth.getInstance().getCurrentUser().getUid();
        DatabaseReference chatIndices = FirebaseDatabase.getInstance()
                .getReference()
                .child("chatIndices")
                .child(uid);
        for (DataSnapshot snapshot : mChatKeys) {
            chatIndices.child(snapshot.getKey()).setValue(true);
            DatabaseReference chat = FirebaseDatabase.getInstance()
                    .getReference()
                    .child("chats")
                    .child(snapshot.getKey());
            chat.child("uid").setValue(uid);
            chat.child("name").setValue("User " + uid.substring(0, 6));
        }
        return null;
    }
}

@harvitronix and everyone on this thread, I'm trying to figure out the right API design so what do you guys think of the Java code above? @samtstern if you have time, what do you think?