Notifications & notifications push

Sur mobile, il existe deux type de notifications, les notifications classiques comme lorsque vous tรฉlรฉchargez un fichier ou suite ร  une action spรฉcifique. Et les notifications push qui sont envoyรฉes ร  l'appareil si celui-ci n'utilise pas l'application concernรฉe par la notification, pour tester il est faut donc se trouver sur une autre application ou en mode accueil. Ces notifications push doivent donc รชtre envoyรฉes par une API REST suite ร  des รฉvรจnements particuliers, par exemple un nouveau tweet pour Twitter, un like sur une publication Facebook...

La maniรจre la plus simple de gรฉrer ces notifications est d'envoyer l'รฉvรจnement ร  Firebase Cloud Messaging, service de Firebase, pour que celui-ci envoit la notification push ร  tous les appareils concernรฉs. Il est possible d'envoyer une notification ร  l'ensemble des appareils possรฉdant l'application ou de cibler un groupe ou un appareil spรฉcifique ร  travers ce qui est appelรฉ les topics (groupe d'appareil) ou les tokens.

Note : Firebase Cloud Messaging est souvent rรฉfรฉrencรฉ en tant que FCM.

WARNING

Si l'application n'est pas en APP_ENV=production, la notification push sera envoyรฉe en topics/test qui ne sera pas reรงu par l'application si elle a รฉtรฉ installรฉe en mode release (comme depuis le Google Play ou via flutter install). Une notification push n'est donc visible qu'en mode debug, avec flutter run, si elle est envoyรฉe depuis l'application back-end en local.

TODO:

Get token

POST https://domain.com/api/login

Body

form-data

email: user@mail.com
password: password
device_name: device_name

Authentification

TODO:

  • get google-services.json and why
  • about clean project for server key refresh

Topics & channels Android

TODO:

  • about topics (set it on creation, subscription on app)
  • about android channels

FCM can send push notifications to application by specific topic. A topic is a channel (not a notification android channel, it's FCM channel) created by FCM on Flutter application. You can create multiple topics where current user will be subscribed. If user is subscribed to a topic, they receive push notification, otherwise they won't receive it. You can check lib/services/NotificationProvider.dart and you will see how it's works.

// here user will receive all push notifications send to '/topics/new_content'
messaging.subscribeToTopic('new_content');

With a little trick, you can define a user's topic.

// here push notification can be send just to 'investor_id_${investor.id}'
messaging.subscribeToTopic(userTopic);

And just for test, when you have a debug app, you can use test topic. Just debug applications will receive this notification.

messaging.subscribeToTopic('test');

Android channels

It's jsut for Android, you can create multiple channels for type of push notification and user can mute a channel to stop notification but they can receive others notifications. You can check current Android channels on lib/services/NotificationProvider.dart.

Astuce

Si vous ne spรฉcifiez pas de channel Android pour votre notification ou que le channel n'existe pas, c'est le channel par dรฉfaut qui sera utilisรฉ.

const AndroidNotificationChannel newSubscriptionsOpened =
    AndroidNotificationChannel(
        'new_subscriptions_opened',
        'Title',
        'Text',
    );

await flutterLocalNotificationsPlugin
    .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>()
    ?.createNotificationChannel(newSubscriptionsOpened);

Tester les notifications

Attention

Si vous testez l'envoi de notifications sur un topic qui est installรฉ par dรฉfaut sur les releases publiรฉes, tous les utilisateurs verront ces tests. Il est donc plus prudent de crรฉer un topic spรฉcifique sur votre appareil pour tester un envoi par topic.

Un test envoyรฉ par la console de Cloud Messaging ร  l'application aura exactement le mรชme effet, tous les appareils disposant du google-services.json associรฉ au projet receveront la notification.

Conseil : utiliser le topic de test et faire les tests par cURL ou par Postman, ce topic n'est ajoutรฉ que sur une application en mode debug. Il est aussi possible d'envoyer une notification ร  un utilisateur spรฉcifique avec le topic dรฉdiรฉ nommรฉ d'aprรจs son id : investor_id_#.

Cloud Messaging

Il est possible de tester directement l'envoi de notifications dans la console de Cloud Messaging.

TODO

cURL

cURL est la mรฉthode la plus simple pour tester les notifications push. La commande suivante est ร  entrer en bash et ne fonctionnera pas en PowerShell.

curl --location --request POST 'https://fcm.googleapis.com/fcm/send' \
    --header 'Content-Type: application/json' \
    --header 'Authorization:key=fcm_key' \
    --data-raw '{
        "notification": {
            "body": "Notification from curl",
            "title": "You have a new message (curl)."
            "android_channel_id": "misc",
        },
        "priority": "high",
        "to": "/topics/test"
    }'

Pour tester les notifications push sur les applications en production, vous pouvez modifier "to": "/topics/test" pour "to": "/topics/new_content". Attention, cela enverra la notification ร  TOUS les appareils utilisant cette application.

Postman

TODO: postman with FCM API, with REST API

POST https://fcm.googleapis.com/fcm/send

Headers

Authorization

key=fcm_key

Content-Type

application/json

Body

raw

{
  "notification": {
    "body": "Notification from postman",
    "title": "You have a new message (postman).",
    "android_channel_id": "misc",
    "click_action": "FLUTTER_NOTIFICATION_CLICK"
  },
  "priority": "high",
  "data": {
    "click_action": "FLUTTER_NOTIFICATION_CLICK",
    "sound": "default",
    "status": "done",
    "screen": "AnyScreen"
  },
  "to": "/topics/test"
}

Pour tester les notifications push sur les applications en production, vous pouvez modifier "to": "/topics/test" pour "to": "/topics/new_content". Attention, cela enverra la notification ร  TOUS les appareils utilisant cette application.

POST http://domain.com/api/notifications/send

Accept

application/json

Bearer Token

To get token, refer to top of this guide.

{
  "title": "Notification push from Postman", // title of notification
  "body": "About new_content", // body of notification
  "android_channel_id": "misc", // optional, only for notification channel on android
  "to": "/topics/new_content", // FCM topic
  "all_users_for_this_model": 2,
  "data": {
    "click_action": "FLUTTER_NOTIFICATION_CLICK",
    "sound": "default",
    "status": "done",
    "screen": "AnyScreen",
    "screen_data": 10
  }
}

Laravel

Actuellement, aucun package n'est utilisรฉ pour envoyer des notifications ร  FCM, les envois sont faits ร  travers cURL, dans le NotificationProvider.php.

Vous aurez besoin d'une clรฉ, nommรฉe Server key, disponible sur la console de Firebase. Celle-ci est dans le .env de Laravel sur la clรฉ FIREBASE_CLOUD_MESSAGING_SERVER_KEY rรฉfรฉrencรฉe dans config/fcm.php sous le nom server_key.

TODO:

  • en auto: new subscription, new contents

NotificationController.php

  • app/Http/Controllers/Admin/NotificationController.php with send(NotificationRequest $request) via NotificationProvider.php
  • app/Http/Requests/NotificationRequest.php with list of required arguments
  • app/Providers/NotificationProvider.php with sendPushNotification(array $validated) to communicate with Cloud Messaging

Routes

  • routes/api.php
  • Route::post('/notifications/send', ...); useful for manual testing with Postman
  • routes/web.php
  • Route::post('notifications/send', ...); used to automatic sending

Envois automatiques

  • resources/js/mixins/model.js with async onSubmit(refreshData = false)
  • resources/js/views/admin/real-estate-companies/Edit.vue with mixins: [model('real-estate-companies', true)], via model.js
  • resources/js/components/RealEstateCompanyEmailSubscriptionModal.vue with async onSubmit()

Packages

Ces packages sont listรฉs ici ร  titre indicatif, aucun n'est actuellement utilisรฉ.

Redirection

TODO:

  • redirection after opening notification

Articles

Faire attention aux changements trรจs frรฉquents dans l'API de Cloud Messaging

Laravel

<?php

namespace App\Http\Controllers;

use App\Providers\NotificationProvider;
use App\Http\Requests\NotificationRequest;

class NotificationController extends Controller
{
    public function send(NotificationRequest $request)
    {
        $validated = $request->validated();
        if ('production' !== config('app.env')) {
            $validated['to'] = 'test';
        }

        // Get $data from $request
        // For postman verification of valid type for data parameter
        if (isset($request->data)) {
            $data = $request->data;
            if ('string' === gettype($request->data)) {
                $data = json_decode($data, true);
            }
        } else {
            $data = [];
        }

        $response = '';
        // Send to a group of devices by 'investor_id_#' flutter topic
        if (isset($validated['all_users_for_this_model'])) {
            $all_users_for_this_model = $validated['all_users_for_this_model'];
            if ($validated['to'] = 'new_content') {
                $user = User::find($all_users_for_this_model);
                $androidChannel = 'new_content';
            } else {
                return response()->json("Parameter 'to' is not valid for all_users_for_this_model");
            }

            $response = [];
            foreach ($users->toArray() as $key => $investor) {
                $validated['to'] = "user_id_{$investor['id']}";
                $data['screen_data'] = $user->id;
                $responseByNotificationProvider = NotificationProvider::sendPushNotification($validated, $data, true, $androidChannel);
                array_push($response, $responseByNotificationProvider);
            }
            // Send to all devices
        } else {
            $response = NotificationProvider::sendPushNotification($validated, $data);
        }

        return response()->json($response);
    }
}
<?php

namespace App\Providers;

use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;

class NotificationProvider
{
    /**
     * Send push notification to com.group.project.flutter with FCM.
     *
     * @throws BindingResolutionException
     *
     * @return JsonResponse
     */
    public static function sendPushNotification(array $validated, array $data = [], bool $toTopic = true, string $androidChannel = 'Misc')
    {
        $title = $validated['title'];
        $body = $validated['body'];
        $to = $validated['to'];
        if (isset($validated['toTopic'])) {
            $toTopic = $validated['toTopic'];
        }

        if (null === $androidChannel) {
            $androidChannel = $to;
        }

        $url = 'https://fcm.googleapis.com/fcm/send';
        $serverKey = config('firebase.cm_server_key');
        $notification = [
            'body'               => $body,
            'title'              => $title,
            'android_channel_id' => $androidChannel,
            'click_action'       => 'FLUTTER_NOTIFICATION_CLICK',
        ];
        $priority = 'high';
        if ($toTopic) {
            $to = "/topics/$to";
        }
        $arrayToSend = [
            'notification'             => $notification,
            'priority'                 => $priority,
            'data'                     => $data,
            'to'                       => $to,
        ];
        $json = json_encode($arrayToSend);
        $headers = [];
        $headers[] = 'Content-Type: application/json';
        $headers[] = 'Authorization: key='.$serverKey;
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        //Send the request
        $response = curl_exec($ch);
        //Close request
        if (false === $response) {
            exit('FCM Send Error: '.curl_error($ch));
        }
        curl_close($ch);

        $response = json_decode($response);
        $response = (array) $response;

        return [
            'response'     => $response,
            'notification' => $arrayToSend,
        ];
    }

    /**
     * [DEPRECATED]
     * Create notification from https://documentation.onesignal.com/reference/create-notification.
     * Push notification for Flutter application by OneSignal.
     *
     * @return string|bool
     */
    public static function sendNotificationOneSignal($title, $message, string $appId = 'app-id', string $authorization = 'auth_key')
    {
        $content = [
            'en' => $message,
        ];
        $headings = [
            'en' => $title,
        ];
        $subtitle = [
            'en' => 'Subtitle',
        ];
        $hashes_array = [];
        $fields = [
            'app_id'            => $appId,
            'included_segments' => [
                'All',
            ],
            'data' => [
                'foo' => 'bar',
            ],
            'contents'                          => $content,
            'headings'                          => $headings,
            // 'subtitle'                          => $subtitle,
            'android_background_layout'         => 'https://domain.com/images/bg.jpg',
            'message_icon'                      => 'ic_stat_onesignal_default',
            'android_accent_color'              => 'ffce6442',
            'android_channel_id'                => 'android_channel_id',
            'buttons'                           => $hashes_array,
        ];

        $fields = json_encode($fields);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://onesignal.com/api/v1/notifications');
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json; charset=utf-8',
            'Authorization: Basic '.$authorization,
        ]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

        $response = curl_exec($ch);
        curl_close($ch);

        return [
            'response' => json_decode($response),
            'fields'   => json_decode($fields),
        ];
    }
}