Flutter Recipe
Register a Flutter app's APNs and FCM tokens with Native Notify's push API using firebase_messaging, show the Android data messages with flutter_local_notifications, then send to devices or subscribers.
Flutter apps register their native tokens with one HTTP call — no Native Notify SDK required — and show Android pushes with a local-notifications plugin, because Native Notify's Android pushes are FCM data messages.
Setup
flutter pub add firebase_core firebase_messaging flutter_local_notifications http, thenflutterfire configure(this generatesfirebase_options.dartfor Android and iOS).- iOS: enable the Push Notifications capability in Xcode. Native Notify sends iOS pushes straight to APNs with your
.p8key — Firebase does not need an APNs key for this. - Save the same Firebase project's service-account JSON and your Apple
.p8as the app's credentials in Native Notify — Push Credentials.
Register the device and show pushes
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;
const _registerUrl = 'https://app.nativenotify.com/api/universal/device/register';
/// Registers this device's native token: APNs on iOS, FCM on Android.
/// Returns false when the token is not ready yet or the server refused it.
Future<bool> registerUniversalDevice({
required int appId,
required String appToken,
required String deviceId,
String? subscriberId,
}) async {
final messaging = FirebaseMessaging.instance;
// Nothing is shown without permission (tokens are issued either way).
await messaging.requestPermission();
// iOS: the raw APNs token (null until APNs has delivered it). Android: the FCM token.
final String? token = Platform.isIOS ? await messaging.getAPNSToken() : await messaging.getToken();
if (token == null) return false; // not ready yet — the next launch registers
return _post(appId: appId, appToken: appToken, deviceId: deviceId, subscriberId: subscriberId, token: token);
}
/// Android rotates FCM tokens: register the new one under the same device.
StreamSubscription<String> watchUniversalToken({
required int appId,
required String appToken,
required String deviceId,
String? subscriberId,
}) {
return FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
if (!Platform.isAndroid) return; // iOS registers its APNs token, not an FCM token
_post(appId: appId, appToken: appToken, deviceId: deviceId, subscriberId: subscriberId, token: fcmToken);
});
}
Future<bool> _post({
required int appId,
required String appToken,
required String deviceId,
required String token,
String? subscriberId,
}) async {
final res = await http.post(
Uri.parse(_registerUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'appId': appId,
'appToken': appToken,
'deviceId': deviceId, // one stable id per install
'platform': Platform.isIOS ? 'ios' : 'android',
if (subscriberId != null) 'subscriberId': subscriberId,
'tokens': Platform.isIOS ? {'apnsToken': token} : {'fcmToken': token},
}),
);
if (res.statusCode != 201) {
// 400/403 answer {"error":{"code","message"}}; a wrong appId/appToken answers 401 as plain text.
debugPrint('Native Notify register failed (${res.statusCode}): ${res.body}');
return false;
}
return true;
}
final _localNotifications = FlutterLocalNotificationsPlugin();
/// Android pushes are FCM *data* messages — show them yourself.
Future<void> showNativeNotifyMessage(RemoteMessage message) async {
final data = message.data; // every value is a String
final title = data['title'];
if (title == null) return;
await _localNotifications.initialize(
const InitializationSettings(android: AndroidInitializationSettings('@mipmap/ic_launcher')),
);
await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
data['message'],
NotificationDetails(
android: AndroidNotificationDetails(
data['channelId'] ?? 'default',
'Notifications',
importance: Importance.high,
priority: Priority.high,
),
),
payload: data['body'], // your pushData JSON (+ nn_notification_id / nn_source)
);
}
- iOS registers the raw APNs token (
getAPNSToken()isnulluntil APNs has delivered it — the next launch registers then); Android registers the FCM token. An iOS FCM token is not accepted (token_platform_mismatch), so the code never sends one. - The data message carries
title,messageandbody(yourpushDataas a JSON string, withnn_notification_id/nn_source), pluschannelId,subtitle, … when the send set them — all strings. The display call uses flutter_local_notifications' long-standinginitialize/showAPI.
Wire it up in main()
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/widgets.dart';
import 'native_notify_push.dart';
// Runs in its own isolate when a data message arrives in the background.
('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async {
await Firebase.initializeApp(); // pass your DefaultFirebaseOptions.currentPlatform here
await showNativeNotifyMessage(message);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); // pass your DefaultFirebaseOptions.currentPlatform here
if (Platform.isAndroid) {
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
FirebaseMessaging.onMessage.listen(showNativeNotifyMessage);
} else {
// iOS shows the APNs alert itself — also while the app is open:
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true);
}
const appId = 123; // your app id + token (e.g. from --dart-define)
const appToken = 'yourAppToken';
const deviceId = 'a-stable-per-install-id'; // e.g. a UUID saved with shared_preferences
await registerUniversalDevice(appId: appId, appToken: appToken, deviceId: deviceId);
watchUniversalToken(appId: appId, appToken: appToken, deviceId: deviceId);
runApp(const SizedBox()); // your app
}
Keep deviceId stable: generate a UUID once per install and store it (for example with shared_preferences) — never a new one per launch.
On iOS the system shows the APNs alert. firebase_messaging only forwards FCM-originated notifications to onMessage / onMessageOpenedApp, so Native Notify's direct-APNs pushes do not reach those Dart handlers; handle iOS taps in the AppDelegate's UNUserNotificationCenter delegate (your pushData keys are at the top level of userInfo — see the Swift recipe) and pass them over a method channel.
Send
curl -X POST https://app.nativenotify.com/api/universal/notifications/send \
-H "Content-Type: application/json" \
-d '{"appId":123,"appToken":"yourAppToken","title":"Hello Flutter","message":"Sent through APNs/FCM.","audience":{"type":"all"}}'
"all" reaches every registered device, with or without a subscriberId; to reach one logged-in user's devices, send to {"type":"subscribers","subscriberIds":[...]} — see Send Notifications.
Next steps
- Device Registration — full field rules (
deviceId,subscriberId, token shapes). - Verify Delivery — test-send and health counts.
- Groups — let users subscribe to topic-like groups from the app (
POST /api/universal/groups/subscribe), then send withaudience: { "type": "group", "key": "…" }.