Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ public class FlutterFirebaseMessagingPlugin
private Observer<RemoteMessage> remoteMessageObserver;
private final LiveData<String> liveDataToken = FlutterFirebaseTokenLiveData.getInstance();
private Observer<String> tokenObserver;
private final LiveData<String> liveDataRegistered =
FlutterFirebaseRegisteredLiveData.getInstance();
private Observer<String> registeredObserver;
private final LiveData<String> liveDataUnregistered =
FlutterFirebaseUnregisteredLiveData.getInstance();
private Observer<String> unregisteredObserver;

private RemoteMessage initialMessage;
// We store the initial notification in a separate variable
Expand All @@ -77,10 +83,14 @@ private void initInstance(BinaryMessenger messenger) {
channel.invokeMethod("Messaging#onMessage", content);
};
tokenObserver = token -> channel.invokeMethod("Messaging#onTokenRefresh", token);
registeredObserver = fid -> channel.invokeMethod("Messaging#onRegistered", fid);
unregisteredObserver = fid -> channel.invokeMethod("Messaging#onUnregistered", fid);
// We remove these observers in the onDetachedFromEngine method. Using "observeForever()"
// allows us to use without a LifecycleOwner.
liveDataRemoteMessage.observeForever(remoteMessageObserver);
liveDataToken.observeForever(tokenObserver);
liveDataRegistered.observeForever(registeredObserver);
liveDataUnregistered.observeForever(unregisteredObserver);

registerPlugin(channelName, this);
}
Expand All @@ -95,6 +105,8 @@ public void onAttachedToEngine(FlutterPluginBinding binding) {
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
liveDataToken.removeObserver(tokenObserver);
liveDataRemoteMessage.removeObserver(remoteMessageObserver);
liveDataRegistered.removeObserver(registeredObserver);
liveDataUnregistered.removeObserver(unregisteredObserver);
}

@Override
Expand Down Expand Up @@ -163,6 +175,38 @@ private Task<Map<String, Object>> getToken() {
return taskCompletionSource.getTask();
}

private Task<Void> register() {
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();

cachedThreadPool.execute(
() -> {
try {
Tasks.await(FirebaseMessaging.getInstance().register());
taskCompletionSource.setResult(null);
} catch (Exception e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

private Task<Void> unregister() {
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();

cachedThreadPool.execute(
() -> {
try {
Tasks.await(FirebaseMessaging.getInstance().unregister());
taskCompletionSource.setResult(null);
} catch (Exception e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

private Task<Void> subscribeToTopic(Map<String, Object> arguments) {
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();

Expand Down Expand Up @@ -482,6 +526,12 @@ public void onMethodCall(final MethodCall call, @NonNull final Result result) {
case "Messaging#getToken":
methodCallTask = getToken();
break;
case "Messaging#register":
methodCallTask = register();
break;
case "Messaging#unregister":
methodCallTask = unregister();
break;
case "Messaging#subscribeToTopic":
methodCallTask = subscribeToTopic(call.arguments());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ public void onNewToken(@NonNull String token) {
FlutterFirebaseTokenLiveData.getInstance().postToken(token);
}

@Override
public void onRegistered(@NonNull String installationId) {
FlutterFirebaseRegisteredLiveData.getInstance().postFid(installationId);
}

@Override
public void onUnregistered(@NonNull String installationId) {
FlutterFirebaseUnregisteredLiveData.getInstance().postFid(installationId);
}

@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
// Added for commenting purposes;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2026 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.firebase.messaging;

import androidx.lifecycle.LiveData;

public class FlutterFirebaseRegisteredLiveData extends LiveData<String> {
private static FlutterFirebaseRegisteredLiveData instance;

public static FlutterFirebaseRegisteredLiveData getInstance() {
if (instance == null) {
instance = new FlutterFirebaseRegisteredLiveData();
}
return instance;
}

public void postFid(String fid) {
postValue(fid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2026 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.firebase.messaging;

import androidx.lifecycle.LiveData;

public class FlutterFirebaseUnregisteredLiveData extends LiveData<String> {
private static FlutterFirebaseUnregisteredLiveData instance;

public static FlutterFirebaseUnregisteredLiveData getInstance() {
if (instance == null) {
instance = new FlutterFirebaseUnregisteredLiveData();
}
return instance;
}

public void postFid(String fid) {
postValue(fid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FirebaseMessagingInstallationIdEnabled</key>
<true/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ import 'token_monitor.dart';
/// 2. Run `melos bootstrap` in FlutterFire project.
/// 3. In your terminal, root to ./packages/firebase_messaging/firebase_messaging/example directory.
/// 4. Run `flutterfire configure` in the example/ directory to setup your app with your Firebase project.
/// 5. Open `token_monitor.dart` and change `vapidKey` to yours.
/// 5. Open `token_monitor.dart` and change `messagingVapidKey` to yours.
/// 6. Run the app on an actual device for iOS, android is fine to run on an emulator.
/// 7. Use the following script to send a message to your device: scripts/send-message.js. To run this script,
/// you will need nodejs installed on your computer. Then the following:
/// a. Download a service account key (JSON file) from your Firebase console, rename it to "google-services.json" and add to the example/scripts directory.
/// b. Ensure your device/emulator is running, and run the FirebaseMessaging example app using `flutter run`.
/// c. Copy the token that is printed in the console and paste it here: https://github.com/firebase/flutterfire/blob/01b4d357e1/packages/firebase_messaging/firebase_messaging/example/lib/main.dart#L32
/// c. Copy the FID that is printed in the console and paste it here: https://github.com/firebase/flutterfire/blob/01b4d357e1/packages/firebase_messaging/firebase_messaging/example/lib/main.dart#L32
/// c. From your terminal, root to example/scripts directory & run `npm install`.
/// d. Run `npm run send-message` in the example/scripts directory and your app will receive messages in any state; foreground, background, terminated.
/// Note: Flutter API documentation for receiving messages: https://firebase.google.com/docs/cloud-messaging/flutter/receive
Expand Down Expand Up @@ -150,10 +150,10 @@ class MessagingExampleApp extends StatelessWidget {
int _messageCount = 0;

/// The API endpoint here accepts a raw FCM payload for demonstration purposes.
String constructFCMPayload(String? token) {
String constructFCMPayload(String? fid) {
_messageCount++;
return jsonEncode({
'token': token,
'fid': fid,
'data': {
'via': 'FlutterFire Cloud Messaging!!!',
'count': _messageCount.toString(),
Expand All @@ -172,7 +172,7 @@ class Application extends StatefulWidget {
}

class _Application extends State<Application> {
String? _token;
String? _fid;
String? initialMessage;
bool _resolved = false;

Expand Down Expand Up @@ -205,8 +205,8 @@ class _Application extends State<Application> {
}

Future<void> sendPushMessage() async {
if (_token == null) {
print('Unable to send FCM message, no token exists.');
if (_fid == null) {
print('Unable to send FCM message, no FID exists.');
return;
}

Expand All @@ -216,7 +216,7 @@ class _Application extends State<Application> {
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: constructFCMPayload(_token),
body: constructFCMPayload(_fid),
);
print('FCM request for device sent!');
} catch (e) {
Expand Down Expand Up @@ -248,6 +248,26 @@ class _Application extends State<Application> {
);
}
break;
case 'register':
{
print('FlutterFire Messaging Example: Registering with FCM.');
await FirebaseMessaging.instance.register(
vapidKey: messagingVapidKey,
);
print(
'FlutterFire Messaging Example: Registering with FCM successful.',
);
}
break;
case 'unregister':
{
print('FlutterFire Messaging Example: Unregistering from FCM.');
await FirebaseMessaging.instance.unregister();
print(
'FlutterFire Messaging Example: Unregistering from FCM successful.',
);
}
break;
case 'get_apns_token':
{
if (defaultTargetPlatform == TargetPlatform.iOS ||
Expand Down Expand Up @@ -285,6 +305,14 @@ class _Application extends State<Application> {
value: 'unsubscribe',
child: Text('Unsubscribe to topic'),
),
const PopupMenuItem(
value: 'register',
child: Text('Register with FCM'),
),
const PopupMenuItem(
value: 'unregister',
child: Text('Unregister from FCM'),
),
const PopupMenuItem(
value: 'get_apns_token',
child: Text('Get APNs token (Apple only)'),
Expand Down Expand Up @@ -315,15 +343,36 @@ class _Application extends State<Application> {
),
),
MetaCard(
'FCM Token',
TokenMonitor((token) {
_token = token;
return token == null
? const CircularProgressIndicator()
: SelectableText(
token,
'FCM Registration FID',
RegistrationMonitor((fid) {
_fid = fid;
return Column(
children: [
if (fid == null)
const Text('No FID registered yet.')
else
SelectableText(
fid,
style: const TextStyle(fontSize: 12),
);
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
ElevatedButton(
onPressed: () => onActionSelected('register'),
child: const Text('Register with FCM'),
),
ElevatedButton(
onPressed: () => onActionSelected('unregister'),
child: const Text('Unregister from FCM'),
),
],
),
],
);
}),
),
ElevatedButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,73 @@

// ignore_for_file: require_trailing_commas

import 'dart:async';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';

/// Manages & returns the users FCM token.
/// Web push certificate key used by the example app.
///
/// This is only required by web, and is ignored on other platforms.
const String messagingVapidKey = 'VAPID_KEY';

/// Manages and returns the app instance FCM registration FID.
///
/// Also monitors token refreshes and updates state.
class TokenMonitor extends StatefulWidget {
/// Also monitors registration changes and updates state.
class RegistrationMonitor extends StatefulWidget {
// ignore: public_member_api_docs
TokenMonitor(this._builder);
RegistrationMonitor(this._builder);

final Widget Function(String? token) _builder;
final Widget Function(String? fid) _builder;

@override
State<StatefulWidget> createState() => _TokenMonitor();
State<StatefulWidget> createState() => _RegistrationMonitor();
}

class _TokenMonitor extends State<TokenMonitor> {
String? _token;
late Stream<String> _tokenStream;
class _RegistrationMonitor extends State<RegistrationMonitor> {
String? _fid;
StreamSubscription<String>? _registeredSubscription;
StreamSubscription<String>? _unregisteredSubscription;

void setToken(String? token) {
print('FCM Token: $token');
void setRegisteredFid(String fid) {
print('FCM registered FID: $fid');
setState(() {
_token = token;
_fid = fid;
});
}

void setUnregisteredFid(String fid) {
print('FCM unregistered FID: $fid');
setState(() {
if (_fid == fid) {
_fid = null;
}
});
}

@override
void initState() {
super.initState();
_registeredSubscription =
FirebaseMessaging.instance.onRegistered.listen(setRegisteredFid);
_unregisteredSubscription =
FirebaseMessaging.instance.onUnregistered.listen(setUnregisteredFid);
FirebaseMessaging.instance
.getToken(
vapidKey:
'BNKkaUWxyP_yC_lki1kYazgca0TNhuzt2drsOrL6WrgGbqnMnr8ZMLzg_rSPDm6HKphABS0KzjPfSqCXHXEd06Y')
.then(setToken);
_tokenStream = FirebaseMessaging.instance.onTokenRefresh;
_tokenStream.listen(setToken);
.register(vapidKey: messagingVapidKey)
.catchError((Object error) {
print('FCM registration failed: $error');
});
}

@override
void dispose() {
_registeredSubscription?.cancel();
_unregisteredSubscription?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
return widget._builder(_token);
return widget._builder(_fid);
}
}
Loading
Loading