push_notification_kit 0.4.0
push_notification_kit: ^0.4.0 copied to clipboard
Auth-agnostic Flutter push notifications: FCM token lifecycle, rich notifications (images & action buttons), deep links, and pluggable backend registration. Bring your own auth via a TokenProvider.
push_notification_kit #
Auth-agnostic Flutter push notifications: FCM token lifecycle, rich
notifications (images & action buttons), deep links, and pluggable backend
registration. Bring your own auth via a TokenProvider; bring your own backend
via a DeviceRegistrar. The package never owns credentials and never assumes an
identity provider, so it works with Keycloak, Firebase Auth, an API key, or
anything else.
Extracted from a production app; designed to pair with the platform Push Notification service but usable against any backend.
Install #
dependencies:
push_notification_kit: ^0.1.0
You must set up Firebase in the host app (firebase_core, firebase_options.dart,
platform config files) — the package does not bundle Firebase project config.
Quick start #
final push = PushKit(
// 1. Where device tokens are registered. HttpDeviceRegistrar authenticates
// with a bearer token from your TokenProvider.
registrar: HttpDeviceRegistrar(
registerUrl: Uri.parse('https://api.example.com/users/add-push-token'),
tokenProvider: CallbackTokenProvider(
getToken: () => auth.accessToken(),
refreshToken: () => auth.forceRefresh(),
),
// Legacy backend expecting {"pushToken": "..."}; omit for the full device JSON.
tokenBodyKey: 'pushToken',
),
// 2. Register only while signed in; clear on sign-out.
authState: authCubit.stream.map((s) => s is Authenticated),
// 3. Android channel (iOS categories are configured natively — see below).
channel: const NotificationChannelConfig(
channelId: 'default',
channelName: 'Notifications',
androidIcon: '@drawable/ic_stat_notify',
),
// 4. Routing.
onTap: (m) => router.go(m.deepLink ?? '/'),
onAction: (actionId, m) => handleAction(actionId, m),
// Engagement: report opens/clicks so the backend can compute open/click rates.
reporter: HttpEngagementReporter(
eventUrl: Uri.parse('https://api.example.com/users/notification-event'),
tokenProvider: myTokenProvider,
),
// 5. iOS action buttons: register categories whose identifiers match the
// `category` you send. Android ignores this (buttons come from `actions`).
iosCategories: const [
DarwinNotificationCategory('participation_invite', actions: [
DarwinNotificationAction.plain('accept', 'Accept',
options: {DarwinNotificationActionOption.foreground}),
DarwinNotificationAction.plain('decline', 'Decline',
options: {DarwinNotificationActionOption.foreground}),
]),
],
);
await push.init();
// After runApp (router/DI ready): dispatch a cold-start tap/action that launched
// the app from a terminated state. Fires onAction for an action-button cold start,
// onTap otherwise. No-op if the app wasn't launched from a notification.
await push.processPendingLaunch();
Background / terminated messages #
@pragma('vm:entry-point')
Future<void> _bgHandler(RemoteMessage message) async {
await Firebase.initializeApp();
await renderBackgroundMessage(
message,
channel: const NotificationChannelConfig(
channelId: 'default', channelName: 'Notifications',
),
);
}
void main() {
FirebaseMessaging.onBackgroundMessage(_bgHandler);
// ...
}
What the package handles #
- Permission request; iOS APNS-token wait+retry before fetching the FCM token.
onTokenRefreshre-registration; register-only-when-authenticated; re-check on app resume.- Foreground policy: render a local notification, call back to your app, or ignore.
- Rich rendering: downloads
imageUrland shows it as an Android BigPicture; rendersactionsas Android action buttons; passescategoryto iOS. - Deep-link taps (
onTap) and action-button taps (onAction); cold-start viagetInitialMessage.
What stays in your app #
-
Firebase project config and initialization.
-
Navigation inside
onTap/ business actions insideonAction. -
iOS images require a Notification Service Extension (a native target the package cannot ship). Add one and mutate the payload to attach the image:
// NotificationService.swift import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent), let urlString = request.content.userInfo["fcm_options"] as? [String: Any], let imageURL = urlString["image"] as? String, let url = URL(string: imageURL) else { return contentHandler(request.content) } URLSession.shared.downloadTask(with: url) { tmp, _, _ in if let tmp = tmp, let attachment = try? UNNotificationAttachment(identifier: "image", url: tmp) { content.attachments = [attachment] } contentHandler(content) }.resume() } }iOS action buttons are registered via the
iosCategoriesparameter (above) — no AppDelegate changes needed; the identifiers must match thecategoryyou send.
Testing #
flutter test # model parsing + HttpDeviceRegistrar (auth/retry) with MockClient
PushKit itself is exercised via the example app against a real Firebase project.