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.
Add the package
Unzip the download into your project's root folder (this creates
PushConsoleSDK/), set up Firebase withflutterfire configure, then:pubspec.yaml# pubspec.yaml dependencies: firebase_core: ^4.0.0 firebase_messaging: ^16.0.0 push_console: path: ./PushConsoleSDK/flutter/push_consoleAndroid: 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.
Initialize in main.dart
lib/main.dartimport '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).Assign the user at login
Dartawait PushConsole.login('user-123'); // after login await PushConsole.logout(); // on logout await PushConsole.setTags({'plan': 'vip'}); await PushConsole.endCall(callId); // end the calliOS: 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.swiftimport 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() } } }Android: show on the lock screen
In
AndroidManifest.xml, addandroid:showWhenLocked="true"andandroid:turnScreenOn="true"to<activity android:name=".MainActivity">. Android 14+: callPushConsole.canUseFullScreenIntent()/requestFullScreenIntentPermission().
✅ After integrating, check
- Run the app on a real device with logging on: you should see
[PushConsole] Device registered …. - The project's Devices tab: the device appears; iOS devices show the VoIP badge if you use calls.
- The Send tab: send a test to that device.
- Choose 📞 Call and test with the app open, closed and with the screen locked.