Flutter: Push уведомления firebase

С PUSH уведомлениями, во Flutter, вроде бы всё и просто, но вот я провозился три дня, чтобы заставить приложение стабильно их показывать. Большая часть проблем вытекает из-за постоянно развивающейся кодовой базы Flutter, в результате чего большая часть примеров в сети — уже не рабочие.

Проблема №1

Пакет flutter_webview_plugin_ios_android, который оказался не совместим с вызовом FirebaseMessaging.onBackgroundMessage, после инициализации которого, перестали работать кэлбеки webview на переходы на другие url (GitHub Issue).

Выход: не бросаться на простой в использовании пакет, а использовать максимально популярный webview_flutter. Хотя и оный у меня заработал, только в самой последней версии — до этого были проблемы с отображением некоторых сайтов с не понятными SSL сертификатами.

Проблема №2

Вызов ensureInitialized:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
...  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
...

который у «всех работает а у меня не работает». И вызывает ошибку

Unhandled Exception: [core/duplicate-app] A Firebase App named "[DEFAULT]" already exists

Пришлось обернуть в:

  try {
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  } catch (e) {}

Проблема №3

Подобрать комбинацию пакетов Flutter и окружение Android Studion, чтобы «всё заработало». В итоге удовлетворительно заработало при используемых версия пакетов Flutter:

firebase_messaging: ^14.4.0
flutter_local_notifications: ^13.0.0

И зависимостей build.grade для Android Studio:

\android\app\build.gradle:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.yandex.android:maps.mobile:4.2.2-full'
    implementation 'com.google.firebase:firebase-bom:31.4.0'
    implementation 'com.google.firebase:firebase-messaging:23.1.2'
    implementation 'com.android.support:multidex:1.0.3'
}

\android\build.gradle:

buildscript {
    ext.kotlin_version = '1.6.10'
    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:7.1.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:4.3.15'
    }
}

Итак, вот текущая минимальная обвязка для работы с PUSH уведомлениями получилась:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'firebase_options.dart';
import 'permissions.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

FirebaseMessaging messaging = FirebaseMessaging.instance;
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  await setupFlutterNotifications();
  showFlutterNotification(message);
  print('Handling a background message ${message.messageId}');
}

late AndroidNotificationChannel channel;
bool isFlutterLocalNotificationsInitialized = false;
Future<void> setupFlutterNotifications() async {
  if (isFlutterLocalNotificationsInitialized) {return;}
  channel = const AndroidNotificationChannel(
    'high_importance_channel', // id
    'High Importance Notifications', // title
    description: 'High Importance Notifications', // description
    importance: Importance.high,
  );
  flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
  await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
  await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true,);
  isFlutterLocalNotificationsInitialized = true;
}
void showFlutterNotification(RemoteMessage message) {
  RemoteNotification? notification = message.notification;
  AndroidNotification? android = message.notification?.android;
  if (notification != null && android != null && !kIsWeb) {
    print("Пришло ПУШ:");
    print(notification.title);
    flutterLocalNotificationsPlugin.show(
      notification.hashCode,
      notification.title,
      notification.body,
      NotificationDetails(
        android: AndroidNotificationDetails(
          channel.id,
          channel.name,
          channelDescription: channel.description,
          icon: 'launch_background',
        ),
      ),
    );
  }
}
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized(); // требуется для PUSH сервиса
  // инициализация PUSH уведомлений
  try {
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  } catch (e) {}
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  await setupFlutterNotifications();
  // получаем токен уведомлений
  FirebaseMessaging.instance.getToken().then((token) {
    print("Token: $token"); // Выплюнем токен в консольку
  });
  runApp(MyApp());
}
...
...
 @override
  Widget build(BuildContext context) {
    String? _token;
    String? initialMessage;
    bool _resolved = false;
    FirebaseMessaging.instance.getInitialMessage().then(
      value) => () {
            _resolved = true;
            initialMessage = value?.data.toString();
      },
    );
    FirebaseMessaging.onMessage.listen(showFlutterNotification); // показ PUSH уведомлений при открытом приложении
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('A new onMessageOpenedApp event was published!');
    });
....
....

Отправка PUSH уведомлений в приложение IOS

Задача: в приложение на IOS отправить push уведомление из PHP

Решение:

public function SendPushIOS($token,$title,$message){     
     $rs=false;
     $notification_payload = [
        "aps" => [
            "alert" => [
                //"title" => "Зарядись!",
                "body" => $message,
               // "action-loc-key" => "PLAY"
            ],
            "badge" => 0,
            "sound" => "bingbong.aiff"
        ]
     ];     
        $token_key = $this->server_key_ios;
        //echo "!! token_id IOS: $this->server_token_id_ios\n";
        $jwt_header = [
                'alg' => 'ES256', 
                'kid' => $this->server_token_id_ios
        ];
        //echo "!!team_id:$this->server_team_id_ios\n";
        $jwt_payload = [
                'iss' => $this->server_team_id_ios, 
                'iat' => time()
        ];
        $raw_token_data = self::b64($jwt_header, true).".".self::b64($jwt_payload, true);
        $signature = '';        
        openssl_sign($raw_token_data, $signature, $token_key, 'SHA256');
        $jwt = $raw_token_data.".".self::b64($signature);
        // send push
        $request_body = json_encode($notification_payload);
        $endpoint = 'https://api.push.apple.com/3/device';
        $url = "$endpoint/$token";
        $ch = curl_init($url);
        //echo "!!server_bandle_id_ios:$this->server_bandle_id_ios\n";
        curl_setopt_array($ch, [
                CURLOPT_POSTFIELDS => $request_body,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
                CURLOPT_HTTPHEADER => [
                        "content-type: application/json",
                        "authorization: bearer $jwt",
                        "apns-topic: $this->server_bandle_id_ios"
                ]
        ]);
        $result_curl=curl_exec($ch);
        $result = json_decode($result_curl);

        if (is_null($result)) {
            $log= new TLog($this->api);
            $log->InsertLogBoiler([
                "source_id"=> TLog::S_Messages,
                "comment"=>"Ошибка отправки PUSH IOS",
                "raw_package"=>curl_error($ch).$result_curl,
                "reason"=> TLog::R_Error
            ]);           
        } else { 
                if ((int)$result>0){
                //if ($result->success>0){
                  $rs=true;  
                };
        };
        

        curl_close($ch);        
     return $rs;
    }
 public function b64($raw, $json=false){
            if($json) $raw = json_encode($raw);
            return str_replace('=', '', strtr(base64_encode($raw), '+/', '-_')); 
    }   

Отправка PUSH уведомлений в приложение Android из PHP

Для того чтобы отправлять пуш уведомления, необходимо получить ключ установки приложения и знать «ключ сервера». Более подробная информация в консоли Firebase

public function SendPushAndroid($server_key,$token,$title,$message){
        $data = json_encode([
            "to" => $token,
            "notification" => [
                "body" => $message,
                "title" => $title,
                "icon" => "ic_launcher"
            ]
        ]);

        $url = 'https://fcm.googleapis.com/fcm/send';
        $server_key = $server_key;
        $headers = array('Content-Type:application/json','Authorization:key='.$server_key);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        $result = curl_exec($ch);
        var_dump($result);
        if ($result === FALSE) {
            die('Oops! FCM Send Error: ' . curl_error($ch));
        }
        curl_close($ch);        
    }