Firebase Cloud Messaging - Handling logout
Asked Answered
J

13

96

How do I handle situation, when user logs out of my application and I no longer want him to receive notifications to the device.

I tried

FirebaseInstanceId.getInstance().deleteToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)

But I still receive the notifications to my device's registration_id.

I also made sure that this is the token I should delete:

FirebaseInstanceId.getInstance().getToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)

or simply FirebaseInstanceId.getInstance().getToken()).

I also tried FirebaseInstanceId.getInstance().deleteInstanceId(), but then the next time I call FirebaseInstanceId.getInstance.getToken I receive null (it works on the second try).

I guess, after deleteInstanceId I could immediately call getToken() again, but it looks like a hack. And also there's this answer which states that it shouldn't be done, but it proposes deleting the token which apparently doesn't work.

So what is the right method to handle this?

Janiculum answered 3/4, 2017 at 19:56 Comment(1)
Be sure to check Dan Alboteanu answer at the bottom before you dig into implementing one of these solutions; TL;DR most of this should be handled server side, not client side.Wentzel
W
78

Okay. So I managed to do some testing and have concluded the following:

  1. deleteToken() is the counterpart of getToken(String, String), but not for getToken().

It only works if the Sender ID you are passing is a different Sender ID (not the same ID that can be seen in your google-services.json). For example, you want to allow a different Server to send to your app, you call getToken("THEIR_SENDER_ID", "FCM") to give them authorization to send to your app. This will return a different registration token that corresponds only to that specific sender.

In the future, if you chose to remove their authorization to send to your app, you'll then have to make use of deleteToken("THEIR_SENDER_ID", "FCM"). This will invalidate the corresponding token, and when the Sender attempts to send a message, as the intended behavior, they will receive a NotRegistered error.

  1. In order to delete the token for your own Sender, the correct handling is to use deleteInstanceId().

Special mentioning this answer by @Prince, specifically the code sample for helping me with this.

As @MichałK already doing in his post, after calling the deleteInstanceId(), getToken() should be called in order to send a request for a new token. However, you don't have to call it the second time. So long as onTokenRefresh() onNewToken() is implemented, it should automatically trigger providing you the new token.

For short, deleteInstanceId() > getToken() > check onTokenRefresh() onNewToken().

Note: Calling deleteInstanceId() will not only delete the token for your own app. It will delete all topic subscriptions and all other tokens associated with the app instance.


Are you positive you're calling deleteToken() properly? The value for audience should be (also seen from my answer that you linked) is "set to the app server's sender ID". You're passing the getId() value which is not the same as the Sender ID (it contains the app instance id value). Also, how are you sending the message (App Server or Notifications Console)?

getToken() and getToken(String, String) returns different tokens. See my answer here.

I also tried FirebaseInstanceId.getInstance().deleteInstanceId(), but then the next time I call FirebaseInstanceId.getInstance.getToken I receive null (it works on the second try).

It's probably because the first time you're calling the getToken(), it's still being generated. It's just the intended behavior.

I guess, after deleteInstanceId I could immediately call getToken() again, but it looks like a hack.

Not really. It's how you'll get the new generated (provided that it is already generated) token. So I think it's fine.

Wilkes answered 4/4, 2017 at 3:9 Comment(14)
This is the only "sender id" which did not throw an error when I called getToken or deleteToken. When I used the text project id from my firebase console both methods threw. Then I used the numerical id which I found in googleservices.json and it looked like it worked. Then passed the getId() and it didn't throw either. So I I figured that's it.Porosity
As for the hack, I have to call it immediately after deleteInstanceId so it returns null the first time, and then call it during login for it to work. That's why I think it's a hack.Porosity
I'll try and see if I can do some testing later and replicate the behavior. Will get back here if I have the time. Cheers!Wilkes
Thanks, I would really appreciate that.Porosity
Did some stuff. Edited my post. Learned something new at the same time. Let me know if something is not clear. Cheers! :)Wilkes
Thank you for checking it! Honestly, I'm surprised how bad and poorly documented this API is... Will give it a spin, though, thanks!Porosity
is there a way to stop listening the FCM while offline, since deleteInstanceId will return a SERVICE_NOT_AVAILABLE_ERROR in that case, and you will be still registeredNorm
Don't forget to run deleteInstanceId outside the main thread, otherwise, it will end up at catch.Hildie
from the iOS doc FCM: "Avoid calling .getToken(authorizedEntity, scope) unless there is a need to enable multiple senders. Use instanceIDWithHandler: instead...Sidwell
@Wilkes not the same ID that can be seen in your google-services.jso Which ID do you mean? There are too many IDs!Boettcher
@Boettcher A different sender ID. If you have multiple projects, use a sender ID that isn't from the main one.Wilkes
Actually, I just want to remove the token and not the instance ID (I don't want to use deleteInstanceId()). Even if I can refresh it to a new one, it's a good option to me @ALBoettcher
@Wilkes What id I put PROJECT_ID (the one that is generated by Google Console). --- developers.google.com/instance-id/guides/android-implementationBoettcher
@Boettcher I'm not sure what you're asking. The Sender ID is usually the same as the Project Number. Check your Firebase Console > Project Settings > Cloud Messaging TabWilkes
G
37

I did a brief research on what would be the most elegant solution to get back the full control (subscribe and unsubscribe to FCM) as before. Enable and disable the FCM after the user logged in or out.

Step 1. - Prevent auto initialization

Firebase now handle the InstanceID and everything else which need to generate a registration token. First of all you need to prevent auto initialization. Based on the official set-up documentation you need to add these meta-data values to your AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<application>

  <!-- FCM: Disable auto-init -->
  <meta-data android:name="firebase_messaging_auto_init_enabled"
             android:value="false" />
  <meta-data android:name="firebase_analytics_collection_enabled"
             android:value="false" />

  <!-- FCM: Receive token and messages -->
  <service android:name=".FCMService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
  </service>

</application>

Now you disabled the automatic token request process. At the same time you have an option to enable it again at runtime by code.

Step 2. - Implement enableFCM() and disableFCM() functions

If you enable the auto initialization again then you received a new token immediately, so this is a perfect way to implement the enableFCM() method. All subscribe information assigned to InstanceID, so when you delete it then initiate to unsubscribe all topic. On this way you able to implement disableFCM() method, just turn back off auto-init before you delete it.

public class FCMHandler {

    public void enableFCM(){
        // Enable FCM via enable Auto-init service which generate new token and receive in FCMService
        FirebaseMessaging.getInstance().setAutoInitEnabled(true);
    }

    public void disableFCM(){
        // Disable auto init
        FirebaseMessaging.getInstance().setAutoInitEnabled(false);
        new Thread(() -> {
            try {
                // Remove InstanceID initiate to unsubscribe all topic
                // TODO: May be a better way to use FirebaseMessaging.getInstance().unsubscribeFromTopic()
                FirebaseInstanceId.getInstance().deleteInstanceId();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

Step 3. - FCMService implementation - token and message receiving

In the last step you need to receive the new token and send direct to your server. Other hand you'll receive your data message and just do it what you want.

public class FCMService extends FirebaseMessagingService {

    @Override
    public void onNewToken(String token) {
        super.onNewToken(token);
        // TODO: send your new token to the server
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        String from = remoteMessage.getFrom();
        Map data = remoteMessage.getData();
        if (data != null) {
            // TODO: handle your message and data
            sendMessageNotification(message, messageId);
        }
    }

    private void sendMessageNotification(String msg, long messageId) {
        // TODO: show notification using NotificationCompat
    }
}

I think this solution is clear, simple and transparent. I tested in a production environment and it's works. I hope it was helpful.

Geoffrey answered 9/8, 2018 at 22:2 Comment(6)
Hello Janos , if i don't enable the auto init by calling "FirebaseMessaging.getInstance().setAutoInitEnabled(true);" what is the impact ? Will app get "onNewToken" callback or not ?Earthshaking
Oh.. sorry my late answer. Yes, after you enable it again, you get a new token in FCMService via call onNewToken().Jurisconsult
I think this is a good implementation. Also take into account to cleanup tokens that are no longer valid (on the server side) - when it receives - error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered'. Check this codelab example github.com/firebase/friendlychat-web/blob/master/…Sidwell
so observe the auth state and if( user == null) { disableFCM() ...Sidwell
FirebaseInstanceId has been deprecated. FirebaseMessaging.deleteToken task should be used for a token removal.Spectrometer
thanks for code sharing . it should be the accepted answer. some methods are deprecated now. Use these now: FirebaseInstallations.getInstance().delete() FirebaseMessaging.getInstance().deleteToken()Brookes
L
20

I was working on the same problem, when I had done my logout() from my application. But the problem was that after logging out, I was still getting push notifications from Firebase. I tried to delete the Firebase token. But after deleting the token in my logout() method, it is null when I query for it in my login() method. After working 2 days I finally got a solution.

  1. In your logout() method, delete the Firebase token in the background because you can not delete Firebase token from the main thread

    new AsyncTask<Void,Void,Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            try
            {
                FirebaseInstanceId.getInstance().deleteInstanceId();
            } catch (IOException e)
            {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        protected void onPostExecute(Void result) {
            // Call your Activity where you want to land after log out
        }
    }.execute();
    
  2. In your login() method, generate the Firebase token again.

    new AsyncTask<Void,Void,Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            String token = FirebaseInstanceId.getInstance().getToken();
            // Used to get firebase token until its null so it will save you from null pointer exeption
            while(token == null) {
                token = FirebaseInstanceId.getInstance().getToken();
            }
            return null;
        }
        @Override
        protected void onPostExecute(Void result) {
        }
    }.execute();
    
Lucio answered 12/8, 2017 at 6:43 Comment(2)
while(token == null) { busy loop } is really bad for your battery... best is to register for a callback. Anotherr (bad but much better) is to Thread.sleep while waiting for the token.Derian
@AmirUval, Thank you for the suggestion, I will definitely try this and let you knowLucio
S
11

Developers should never unregister the client app as a mechanism for logout or for switching between users, for the following reasons:

  • A registration token isn't associated with a particular logged in user. If the client app unregisters and then re-registers, the app can receive the same registration token or a different registration token.
  • Unregistration and re-registration may each take up to five minutes to propagate. During this time messages may be rejected due to the unregistered state, and messages may go to the wrong user. To make sure that messages go to the intended user:

  • The app server can maintain a mapping between the current user and the registration token.

  • The client app can then check to ensure that messages it receives match the logged in user.

this quote is from a deprecated google documentation

But there is reasons to believe this is still true - even if the documentation above is deprecated.

You can observe this here - check out how they do it in this codelab https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.js

and here https://github.com/firebase/friendlychat-web/blob/master/cloud-functions/public/scripts/main.js

Sidwell answered 2/2, 2019 at 9:13 Comment(5)
You linked and quoted the deprecated GCM docs. I could not find similar info for FCM.Porosity
I have reasons to believe this is also available for FCM. Look how they do it in this codelab codelabs.developers.google.com/codelabs/… . The codelab is updated and maintained. There they never deleteInstanceId(). Instead they update the server (Firestore) with the token - userID(uid) pair in authStateObserver(user) github.com/firebase/friendlychat-web/blob/master/… Also Cleans up the tokens that are no longer valid on serverSidwell
@DanAlboteanu but the code samples you linked don't show what happens when the user logs out. There is no update to the server from what I have seen...Conquian
The link is dead nowThenceforth
The client app can then check to ensure that messages it receives match the logged in user. this is really bad. The client should never decide if they are eligible to receive information. This can easily be hacked to receive all notifications including those from previously logged in users. There should be a way to check in the Cloud Function that sends the notifications if an fcm token is eligible to receive the message.Apteral
E
9

Since the getToken() is deprecated, use getInstanceId() to regenerate new token instead. It has same effect.

public static void resetInstanceId() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                FirebaseInstanceId.getInstance().deleteInstanceId();
                FirebaseInstanceId.getInstance().getInstanceId();   
                Helper.log(TAG, "InstanceId removed and regenerated.");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();
}
Eloquent answered 20/11, 2018 at 13:11 Comment(0)
D
5

Use this methods. This is my solution, and I referred this at here When you sign-up, use initFirebaseMessage,. and when log-out or delete use removeFirebaseMessage().

    private fun removeFirebaseMessage(){
        CoroutineScope(Dispatchers.Default).launch {
            FirebaseMessaging.getInstance().isAutoInitEnabled = false
            FirebaseInstallations.getInstance().delete()
            FirebaseMessaging.getInstance().deleteToken()
        }
    }

    private fun initFirebaseMessage(){
        val fcm = FirebaseMessaging.getInstance()
        fcm.isAutoInitEnabled = true
        fcm.subscribeToTopic("all")
        fcm.subscribeToTopic("")
    }
Dody answered 22/2, 2021 at 7:23 Comment(1)
working on newer firebase version. ThanksZumwalt
H
4

Another handy way to clear the firebase token and regenerated a new one using FirebaseMessaging.getInstance()

fun clearFirebaseToken() {
    FirebaseMessaging.getInstance().apply {
        deleteToken().addOnCompleteListener { it ->
            Log.d("TAG++", "firebase token deleted ${it.result}")
            token.addOnCompleteListener {
                Log.d("TAG++", "firebase token generated ${it.result}")
                if (it.result != null) saveTokenGenerated(it.result!!)
            }
        }
    }
}
Headmistress answered 10/2, 2021 at 19:21 Comment(0)
P
2

Just call deleteToken method on a background Thread upon Logout:

https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#public-void-deletetoken-string-senderid,-string-scope

 FirebaseInstanceId.getInstance().deleteToken(getString(R.string.gcm_defaultSenderId), "FCM")

The first argument takes the SenderID as it is defined in your FireBaseConsole

enter image description here

It takes a few seconds to update - and after that, you will no longer get FCM notifications.

Paymaster answered 6/11, 2019 at 11:10 Comment(0)
S
1

I know I am late for the party. deleteInstanceId() should be called from the background thread since it's a blocking call. Just check the method deleteInstanceId() in FirebaseInstanceId() class.

@WorkerThread
public void deleteInstanceId() throws IOException {
    if (Looper.getMainLooper() == Looper.myLooper()) {
        throw new IOException("MAIN_THREAD");
    } else {
        String var1 = zzh();
        this.zza(this.zzal.deleteInstanceId(var1));
        this.zzl();
    }
}  

You can start an IntentService to delete the instance id and the data associated with it.

Seabrooke answered 26/7, 2018 at 6:18 Comment(1)
This is true but irrelevant here (also other answers wrapped it in asynctask which is basically the same)Porosity
S
1

The firebase.iid package that contains FirebaseInstanceId is now deprecated. Auto-initialization has been migrated from Firebase Instance ID to Firebase Cloud Messaging. Also its behaviour has slighly changed. Before, a call to deleteInstanceId() would automatically generate a new token if auto-initialization was enabled. Now, the new token is only generated on the next app-start or if getToken() is called explicitly.

private suspend fun loginFCM() = withContext(Dispatchers.Default) {
    val fcm = FirebaseMessaging.getInstance()
    fcm.isAutoInitEnabled = true
    fcm.token.await()
}

private suspend fun logoutFCM() = withContext(Dispatchers.Default) {
    val fcm = FirebaseMessaging.getInstance()
    fcm.isAutoInitEnabled = false // To prevent a new token to be generated automatically in the next app-start (remove if you don't care)
    fcm.deleteToken().await()
}

If you want to logout completely from Firebase you can just delete the whole installation afterwards:

private suspend fun logoutFirebase() = withContext(Dispatchers.Default) {
    logoutFCM()
    val firebase = FirebaseInstallations.getInstance()
    firebase.delete().await()
}
Solus answered 29/3, 2021 at 17:16 Comment(0)
H
0

To wrap it all up, use background thread to delete the instanceID, the next time you login keep an eye on the Firestore/Realtime DB (if you save your tokens there), they will refresh

public void Logout() {

        new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    FirebaseInstanceId.getInstance().deleteInstanceId();
                    FirebaseInstanceId.getInstance().getInstanceId();
                } catch (final IOException e) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(Flags.this, e.getMessage(), Toast.LENGTH_SHORT).show();
                        }
                    });
                }
            }
        }.start();
        FirebaseMessaging.getInstance().setAutoInitEnabled(false);
        FirebaseAuth.getInstance().signOut();
        SharedPreferences sharedPreferences = getDefaultSharedPreferences(Flags.this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.clear();
        editor.apply();
        startActivity(new Intent(Flags.this, MainActivity.class));
        Flags.this.finish();
    }
Hoenir answered 24/6, 2020 at 22:27 Comment(0)
S
0

This code below I used it and it helps me, and I used Kotlin coroutine instead of Thread(Runnable{}).start() because it less cost than creating a new thread object

 private fun logoutFromFCM() {
    GlobalScope.launch(Dispatchers.IO) {
        FirebaseInstallations.getInstance().delete()
        FirebaseMessaging.getInstance().deleteToken()

        FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.w(TAG, "Fetching FCM registration token failed", task.exception)
                return@OnCompleteListener
            }

            // Get new FCM registration token
            val token = task.result
            saveFirebaseToken(token)
            Log.w(TAG, "Token Updated - newToken> $token")
        })
    }
}
Stabilizer answered 24/3, 2021 at 11:44 Comment(0)
L
0

For many situations where the notifications requirements are simple, the issue of handling log out can be implemented much more easily. For example, in my case every user is subscribed to only two topics:

  • Global alerts topic
  • User specific topic defined as the users email (with replacement of @ with - because @ is not allowed in topic string)

For such simple scenarios simply unsubscribe from the unwanted topics on log out:

Future<void> signOut() async {
  messaging.unsubscribeFromTopic(emailToTopic(_firebaseAuth.currentUser.email));
  await _firebaseAuth.signOut();
}

And of course subscribe to topics only on successful log in or sign up:

Future<String> signIn({String email, String password}) async {
  try {
    await _firebaseAuth.signInWithEmailAndPassword(
        email: email, password: password);
    messaging.subscribeToTopic(emailToTopic(email));
    return "Signed in";
  } on FirebaseAuthException catch (e) {
    return e.message;
  }
}
Levitical answered 30/5, 2021 at 6:51 Comment(1)
This solution is neat & simple without needing to manage a token on client-side & server-side. There's one drawback though! How would the server know if the user simply uninstalled the app without logging out? If the server continued to send user-specific notifications after the user uninstalled, its all kind of waste!Fantinlatour

© 2022 - 2024 — McMap. All rights reserved.