appcraft_notifications 0.0.1 copy "appcraft_notifications: ^0.0.1" to clipboard
appcraft_notifications: ^0.0.1 copied to clipboard

Firebase Cloud Messaging and local notifications behind one API: foreground copies, taps, cold start and the FCM token, with no backend assumptions.

appcraft_notifications #

Pub Version License: MIT

Firebase Cloud Messaging and local notifications behind one API.

The package receives messages in all three states, shows a local copy of a message that arrives while the application is running, reports taps and hands over the FCM token. It makes no assumption about a backend: registering the token on a server, storing a notification history and navigating by a payload belong to the application, so the package fits any server contract.

Supported platforms: Android, iOS and the web.

Installation #

dependencies:
  appcraft_notifications: ^0.0.1

The application also needs firebase_core and firebase_messaging: it initializes Firebase itself and declares its own background message handler.

Usage #

const AndroidNotificationChannel channel = AndroidNotificationChannel(
  'orders',
  'Orders',
  description: 'Order status',
  importance: Importance.max,
);

@pragma('vm:entry-point')
Future<void> handleBackgroundMessage(RemoteMessage message) async {
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  // Store the message here if the application keeps a history.
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage);

  final AppCraftNotifications notifications = await AppCraftNotifications.create(
    androidChannel: channel,
    androidIcon: 'ic_notification',
  );

  runApp(MyApp(notifications: notifications));
}

The order matters: Firebase is initialized first, the background handler is registered before runApp, and create() is awaited — until it completes messages are not being received. There is no public constructor and no global instance: the application owns the object and passes it around itself.

notifications.messages.listen(showInApp);   // arrived while running
notifications.taps.listen(navigateByData);  // a tap on a notification

// The launch notification does not go through a stream — a broadcast stream
// does not buffer, and it is known before anyone can subscribe. Take it when
// the navigator already exists.
final AppCraftNotification? launch = notifications.takeLaunchNotification();

messages reports data-only messages as well, but a local copy is shown only for a message that carries a title or a body — a data-only one does not become an empty notification in the drawer. The application can show a notification of its own in the same channel:

await notifications.show(
  title: 'Order is ready',
  body: 'Pick it up until 21:00',
  data: <String, String>{'route': '/orders/1'},
);

dispose() cancels the subscriptions and closes the streams; after it the instance is unusable and a new one can be created.

AppCraftNotification is flat: messageId, title, body, data and receivedAt. RemoteMessage does not leak out — after a tap on a locally shown copy and after a cold start it does not exist. By the FCM protocol every value of data is a string; a nested structure comes as a string with JSON inside, and the package decodes one level only and guesses nothing.

Permission #

final AuthorizationStatus status = await notifications.requestPermission();

Choose the moment yourself — the package never asks on its own. One call covers both iOS and POST_NOTIFICATIONS on Android 13+. The system dialog is shown once, and a denial on iOS is final: afterwards the permission can only be granted in the system settings. Without a granted permission iOS issues no APNs token, so getToken() returns null there.

getPermissionStatus() returns the current settings without asking the user.

Token #

getToken() and tokenChanges are everything the package knows about the token. On the web FCM issues it only for the Web Push certificate of the project, so pass vapidKey to create(); without it getToken() throws a StateError there.

Registering the token on a server belongs to the application, and the order of a rotation is what makes it correct:

  1. read the previously stored token;
  2. await register(newToken) — register the new one first;
  3. only after it succeeds, revoke(oldToken) if there was one and it differs;
  4. only then overwrite the stored value.

The reverse order leaves the user without notifications when a request fails; a postponed overwrite gives a natural retry on the next launch.

Notification history #

The package stores nothing. An application that needs a history subscribes to messages and writes on its own — into a database it already has.

Two requirements that no library can solve for you:

  • Deduplicate. The same message reaches the background handler and then the tap handler. The natural key is messageId; insert ignoring a conflict.
  • Do not show a local notification from the background handler for a message with a notification block: in the background FCM has already shown it, and a local copy adds a second one to the drawer. It is admissible for a data-only message and only as a deliberate decision.

Setup #

Android #

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="orders" />

The value must be the id of the channel passed to create(), otherwise the notifications FCM shows itself land in a different channel with a different sound and importance. The channel id is a permanent technical key: Android ignores a change of importance of an existing channel, and changing it requires deleting the channel, which resets what the user has configured. The name is a displayed string and can change freely.

androidIcon is the name of a drawable of the application, without a folder and without an extension.

iOS #

Upload the APNs key in the Firebase console. For data-only messages in the background add remote-notification to UIBackgroundModes in Info.plist.

Web #

final AppCraftNotifications notifications = await AppCraftNotifications.create(
  androidChannel: channel,   // not used on the web, but still required
  androidIcon: 'ic_notification',
  vapidKey: 'YOUR_WEB_PUSH_CERTIFICATE_KEY',
);

Put a web/firebase-messaging-sw.js in the application — copy the one from example/web. It is the background handler of a web application: the Dart function registered through FirebaseMessaging.onBackgroundMessage() is never called on the web.

That worker is also what makes taps on background pushes visible. A tap on a notification shown by Firebase happens while no Dart code is running, and onMessageOpenedApp does not fire on the web, so the worker forwards it: to an open tab as a message, and to a closed one as query parameters of the opened URL. The package receives both through the same path as a tap on a notification it showed itself — taps while the tab is open, takeLaunchNotification() when the tap started the application.

Keep the shape of that file when editing it: the click handler is registered before the Firebase imports on purpose, and it takes the click away from FCM while still honouring fcm_options.link. Both reasons are in the comments of the worker itself.

Nothing has to be registered for the local notifications: the plugin brings its own service worker as an asset and registers it during create(). It needs the Notification API and a granted permission — until both are there, showing is skipped and, with debugLogs, the reason is written to the log.

What is not covered #

  • On Apple platforms a notification history is inherently incomplete: the background handler is not called for a notification-only message when the application is not running.
  • On the web a message that arrives with the tab closed does not pass through the package at all — it belongs to firebase-messaging-sw.js, and so does storing it. Only the tap comes back, and only while the worker forwards it.
  • pub.flutter-io.cn marks the package as not WASM-compatible: flutter_local_notifications exports its Windows details unconditionally, and those import dart:io. Web builds are unaffected, flutter build web --wasm included.
0
likes
150
points
52
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Firebase Cloud Messaging and local notifications behind one API: foreground copies, taps, cold start and the FCM token, with no backend assumptions.

Repository (GitHub)
View/report issues

Topics

#firebase #notifications #push

License

MIT (license)

Dependencies

firebase_messaging, flutter, flutter_local_notifications

More

Packages that depend on appcraft_notifications