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),
        ];
    }
}