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

  1. flutter pub add firebase_core firebase_messaging flutter_local_notifications http, then flutterfire configure (this generates firebase_options.dart for Android and iOS).
  2. iOS: enable the Push Notifications capability in Xcode. Native Notify sends iOS pushes straight to APNs with your .p8 key — Firebase does not need an APNs key for this.
  3. Save the same Firebase project's service-account JSON and your Apple .p8 as 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() is null until 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, message and body (your pushData as a JSON string, with nn_notification_id / nn_source), plus channelId, subtitle, … when the send set them — all strings. The display call uses flutter_local_notifications' long-standing initialize / show API.

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 with audience: { "type": "group", "key": "…" }.