fcm_actions 0.1.0 copy "fcm_actions: ^0.1.0" to clipboard
fcm_actions: ^0.1.0 copied to clipboard

Extensible, testable notification actions for Firebase Cloud Messaging on Android and iOS.

fcm_actions #

Extensible, testable notification actions for Firebase Cloud Messaging on Android and iOS.

fcm_actions normalizes FCM payloads, renders data messages, registers Android channels and iOS categories, routes taps, handles inline replies, and exposes typed event streams. Action IDs remain strings by design, so applications can add actions without modifying this package.

Requirements #

  • Flutter 3.41 or newer
  • Dart 3.11 or newer
  • Android and iOS
  • A Firebase project configured for the consuming application

Installation #

dependencies:
  fcm_actions: ^0.1.0

Run flutterfire configure in the application. On iOS, enable the Push Notifications capability and Background Modes > Remote notifications in Xcode. Those signing entitlements cannot be added by a Dart package; no application delegate or other native source code is required.

flutter_local_notifications requires Java core-library desugaring. In android/app/build.gradle.kts, enable it in compileOptions and add its dependency:

compileOptions {
    isCoreLibraryDesugaringEnabled = true
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

Quick start #

final navigatorKey = GlobalKey<NavigatorState>();

await FcmActions.initialize(
  navigatorKey: navigatorKey,
  actions: const [
    NotificationAction(id: 'accept', title: 'Accept'),
    NotificationAction(id: 'decline', title: 'Decline', destructive: true),
  ],
  routes: const [
    RouteMapper(type: 'chat', route: '/chat'),
  ],
  onAction: (event, data) async {
    debugPrint('${event.actionId}: $data');
  },
);

runApp(MaterialApp(navigatorKey: navigatorKey));

Actions can also be registered at runtime:

FcmActions.registerAction(
  const NotificationAction(
    id: 'download_invoice',
    title: 'Download',
    icon: 'download',
  ),
  handler: (event) async {
    await invoices.download(event.payload['invoice_id'] as String);
  },
);

Payload format #

actions may be a JSON array or a JSON-encoded string because FCM data values are commonly strings.

{
  "message_id": "invite-923",
  "notification_id": 923,
  "title": "Invitation",
  "body": "Join the project?",
  "type": "chat",
  "channel_id": "fcm_actions_high",
  "actions": [
    {"id": "accept", "title": "Accept"},
    {"id": "decline", "title": "Decline", "destructive": true},
    {
      "id": "reply",
      "title": "Reply",
      "input": true,
      "inputLabel": "Type a reply",
      "showsUserInterface": false
    }
  ]
}

An action object is fully dynamic. An ID-only entry such as "accept" resolves an action previously registered by the application.

Events #

FcmActions.onNotification.listen((event) {});
FcmActions.onAction.listen((event) {});
FcmActions.onDismiss.listen((event) {});
FcmActions.onReply.listen((event) => debugPrint(event.replyText));
FcmActions.onForegroundMessage.listen((event) {});
FcmActions.onBackgroundMessage.listen((event) {});
FcmActions.onOpenedApp.listen((event) {});

NotificationActionEvent includes the action ID, normalized payload, notification and message IDs, timestamp, raw JSON, platform, lifecycle state, reply text, and the complete NotificationMessage.

Background actions and replies #

An Android background callback must be a top-level or static entry point:

@pragma('vm:entry-point')
Future<void> notificationActionBackground(
  NotificationActionEvent event,
) async {
  // Use isolate-safe services only.
}

await FcmActions.initialize(
  backgroundActionHandler: notificationActionBackground,
);

If the callback fails or is omitted, the event is persisted and delivered to the main isolate when the app resumes. Failed main-isolate actions are queued and retried on resume, up to maxActionRetries.

iOS actions intentionally use Apple's foreground option so consuming apps do not need AppDelegate plugin-registration code. They still work from background and terminated notifications, but iOS launches the application UI to deliver them.

Local notifications #

Foreground FCM notifications and non-silent data messages are rendered through flutter_local_notifications, ensuring one action path:

await FcmActions.show(
  title: 'Invoice ready',
  body: 'Tap to view',
  deepLink: '/invoices/42',
  actions: const [
    NotificationAction(id: 'download', title: 'Download'),
  ],
);

await FcmActions.cancel(42);
await FcmActions.cancelAll();

Use autoDisplayDataMessages: false when the application wants to render data-only foreground messages itself.

Channels #

Built-in Android channels are NotificationChannel.max, .high, .defaultChannel, .low, and .silent. Add a custom channel before using it:

await FcmActions.registerChannel(
  const NotificationChannel(
    id: 'orders',
    name: 'Orders',
    description: 'Order status updates',
    importance: ChannelImportance.high,
  ),
);

Android channel sound and importance cannot be changed after creation. Use a new channel ID when those settings change.

RouteMapper removes application switch statements. The complete payload is passed as named-route arguments. A deep_link payload value is treated as a named route when no type mapping matches.

FcmActions.registerRoute(
  const RouteMapper(type: 'order', route: '/orders/detail'),
);

Middleware and interceptors #

Middleware composes authentication, logging, analytics, and permission checks. Calling next(event) continues the chain.

final class AuthMiddleware implements NotificationMiddleware {
  @override
  Future<void> handle(
    NotificationActionEvent event,
    Future<void> Function(NotificationActionEvent event) next,
  ) async {
    if (session.isSignedIn) await next(event);
  }
}

Use NotificationInterceptor around display, ActionInterceptor around execution, and NotificationAnalytics to adapt Firebase Analytics, Mixpanel, or another provider. Adapter failures are logged and never crash the app.

Security #

  • Payload size is limited to 32 KiB by default.
  • Action IDs are validated and malformed JSON is rejected.
  • Processed action/message identities persist for seven days to prevent duplicate execution and replay.
  • Internal persistence uses bounded queues.
  • Client payloads are untrusted. Authorize every action on the server; never embed a signing secret in a mobile application.

Platform behavior #

Dynamic actions work immediately for local notifications and FCM data messages. An OS-rendered remote notification received while the app is terminated can only use actions/categories known to the operating system before delivery. Register those actions during initialization and send the matching category_id on iOS. iOS may display fewer actions than supplied based on available UI space.

FCM topic and condition messages need no client-specific handling; they arrive as notification, data, or mixed messages. iOS silent delivery is best-effort and requires content-available: 1 plus the Background Modes entitlement. Force-stopped Android apps and user-terminated iOS apps remain subject to OS delivery restrictions.

Testable API #

The static facade owns one default client. For tests or multiple Firebase apps, construct FcmActionsClient and inject a NotificationEngine. Platform seams and fakes are exported from package:fcm_actions/fcm_actions_testing.dart.

See the complete example, architecture, best practices, FAQ, and migration guide.

11
likes
0
points
167
downloads

Publisher

unverified uploader

Weekly Downloads

Extensible, testable notification actions for Firebase Cloud Messaging on Android and iOS.

Repository (GitHub)
View/report issues

Topics

#firebase #notifications #fcm #actions #messaging

License

unknown (license)

Dependencies

collection, crypto, firebase_core, firebase_messaging, flutter, flutter_local_notifications, freezed_annotation, json_annotation, meta, shared_preferences

More

Packages that depend on fcm_actions