Flutter Push Notifications for iOS & Android

Dart, Flutter 3.22+. Built on firebase_messaging and flutter_callkit_incoming.

💙 Flutter

Dart, Flutter 3.22+. Built on firebase_messaging and flutter_callkit_incoming.

Download the SDK from the dashboard (Integration guide) after you create a free account.

  1. Add the package

    Unzip the download into your project's root folder (this creates PushConsoleSDK/), set up Firebase with flutterfire configure, then:

    pubspec.yaml
    # pubspec.yaml
    dependencies:
      firebase_core: ^4.0.0
      firebase_messaging: ^16.0.0
      push_console:
        path: ./PushConsoleSDK/flutter/push_console

    Android: upload the service account JSON of the same Firebase project to TingPush. iOS: the server sends straight to APNs, so you need a .p8 key.

  2. Initialize in main.dart

    lib/main.dart
    import 'package:firebase_core/firebase_core.dart';
    import 'package:firebase_messaging/firebase_messaging.dart';
    import 'package:flutter/material.dart';
    import 'package:push_console/push_console.dart';
    import 'firebase_options.dart';
    
    @pragma('vm:entry-point')
    Future<void> firebaseBackgroundHandler(RemoteMessage message) async {
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
      await PushConsole.handleBackgroundMessage(message); // Android: show the call screen
    }
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
      FirebaseMessaging.onBackgroundMessage(firebaseBackgroundHandler);
    
      await PushConsole.initialize(
        appId: 'YOUR_APP_ID',
        serverUrl: 'https://tingpush.com',
        callStyle: const PushConsoleCallStyle(appName: 'MyApp'),
      );
    
      PushConsole.onCallEvent.listen((e) {
        if (e.action == PushConsoleCallAction.accepted) {
          // User tapped Answer → connect the call (e.call.id, e.call.data['room'])
        }
      });
      PushConsole.onNotificationOpened.listen((m) { /* navigate using m.data */ });
    
      runApp(const MyApp());
    }

    APNs environment: debug/profile = sandbox, release = production (override with iosSandbox).

  3. Assign the user at login

    Dart
    await PushConsole.login('user-123');       // after login
    await PushConsole.logout();                // on logout
    await PushConsole.setTags({'plan': 'vip'});
    await PushConsole.endCall(callId);         // end the call
  4. iOS: enable capabilities and update AppDelegate (for calls)

    Open ios/Runner.xcworkspace → Signing & Capabilities: add Push Notifications and Background Modes (Remote notifications + Voice over IP). Calls on iOS are VoIP pushes, which must be reported to CallKit in native code:

    ios/Runner/AppDelegate.swift
    import UIKit
    import Flutter
    import PushKit
    import flutter_callkit_incoming
    
    @main
    @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate {
        override func application(_ application: UIApplication,
                                  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            GeneratedPluginRegistrant.register(with: self)
            let voipRegistry = PKPushRegistry(queue: .main)
            voipRegistry.delegate = self
            voipRegistry.desiredPushTypes = [.voIP]
            return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
    
        func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
            let token = credentials.token.map { String(format: "%02x", $0) }.joined()
            SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(token)
        }
    
        func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
            SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
        }
    
        func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
                          for type: PKPushType, completion: @escaping () -> Void) {
            guard type == .voIP else { return completion() }
            let p = payload.dictionaryPayload
            let data = flutter_callkit_incoming.Data(
                id: (p["call_id"] as? String) ?? UUID().uuidString,
                nameCaller: (p["caller_name"] as? String) ?? "",
                handle: (p["caller_handle"] as? String) ?? "",
                type: (p["is_video"] as? String) == "true" ? 1 : 0)
            data.duration = (Int((p["timeout"] as? String) ?? "") ?? 45) * 1000
            var extra: [String: Any] = [:]
            for (key, value) in p { if let key = key as? String, key != "aps" { extra[key] = value } }
            data.extra = extra as NSDictionary
            SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true) {
                completion()
            }
        }
    }
  5. Android: show on the lock screen

    In AndroidManifest.xml, add android:showWhenLocked="true" and android:turnScreenOn="true" to <activity android:name=".MainActivity">. Android 14+: call PushConsole.canUseFullScreenIntent() / requestFullScreenIntentPermission().

✅ After integrating, check

  1. Run the app on a real device with logging on: you should see [PushConsole] Device registered ….
  2. The project's Devices tab: the device appears; iOS devices show the VoIP badge if you use calls.
  3. The Send tab: send a test to that device.
  4. Choose 📞 Call and test with the app open, closed and with the screen locked.

Ready to integrate?

Create an account to get your App ID and API Key and download the SDKs. Dashboard samples are pre-filled with your project details.