Skip to content

Notifications in Hubtel Apps

Notifications form a key part in every app. On Hubtel Apps it's no different, users will benefit from these alerts. In a way it tells users that an app is serious. The same way humans call a longtime friend to check up, that's the same way an app should call a disengaged user.

Overview

What are push notifications

Push notifications are messages that are delivered directly to a smartphone’s notification system via the internet. Usually for this to work, there must be an identifier - a PushId, that uniquely identifies the device. Once a message is sent to this PushId, the current user of that phone, if they still have the app installed, will receive the notification.

A Sample PushId

json
d5UvEibxZEhqrNmRkgzeju:APA91bGKgEnxsKkgbyu_tP5YHzQECRVqC0KCeRYJ7gdCjecXZe5Ocl9S_R1j0zcQ2skWVgWmJH0Woj_MeUNDWoROnKq0aMaJ-YQFlknrrNdThQJWzgN-OKlsDhU6N6Us4HX-iHITokn

These ids often must be stored in a backends database with a pointer to a known property of a user eg phone number. That way if the backend wanted to send a message to a user with phone number 024 123 4567, they will first retrieve the user that has phone number 024 123 4567, find the associate push id, then send the message to the push id then the user will receive it.

This works, but managing PushIds is not always ideal, since they change often, or users change their phones. Per the current trend, most users will change their phone every 2 years or less.

Another approach

Firebase, one of the leading platforms for handling mobile development services and the most popular push notification service provider, released notification topics a while back. Upon release an app was limited to only 1 million topics. However, this is not the case anymore and this provides a great opportunity to change the way notification works for single users.

A push notification topic is just like an email subscription service, where you subscribe for a newsletter, and you keep getting information from that source. A firebase topic does the exact same thing, subscribe to it and you will receive information sent to that topic. In some other sense it's a radio station that you have decided to always listen to.

This changes everything. Because we now create unlimited topics. We can create a topic for each user and notify the topic instead of the user directly. Also, when users log into the app we can subscribe to various topics, which we can later use to reach them. Some of these topics are static in the sense that it's the same for everyone e.g. All users Topic (every user will be added to this topic). Some are dynamic, in the sense that they have a unique identifier added to a prefix, this makes it unique for a group of users.

js
//for user only eg user_233241033274
String userTopic;

//for user only eg station_{station_id}
String stationTopic;


//all users eg all_users
String allUsersTopic;

Sample Topic Implementation

js
class FirebaseNotificationManager {
  var messaging = FirebaseMessaging.instance;

  subscribeToTopics({required List<String> topics}) async {
    for (var topic in topics) {
      await messaging.subscribeToTopic(topic);
    }
  }

  unSubscribeToTopics({required List<String> topics}) async {
    List<Future> unsubscribeRequests = [];
    for (var topic in topics) {
      unsubscribeRequests.add(messaging.unsubscribeFromTopic(topic));
    }

    try {
      await Future.wait(unsubscribeRequests.map((unsubscribeRequest) =>
          unsubscribeRequest.timeout(Duration(seconds: AppConstants.unsubscribeTimeout), onTimeout: () {
            print("Unsubscription timed out");
          })));
    } catch (e) {
      print("Timeout on unsubscription");
      print(e);
    }
  }

  static subscribeUserToAppTopics() async {
    var firebaseNotificationManager = FirebaseNotificationManager();
    var manager = Manager();
    var userResult = await manager.getUser();
    var user = userResult.result;

    if (user == null) {
      return;
    }

    //owner topics
    await firebaseNotificationManager.subscribeToTopics(topics: user.getAllAppNotificationTopics());    
  }

  static unSubscribeUserFromAppTopics() async {
    var firebaseNotificationManager = FirebaseNotificationManager();
    var manager = Manager();
    var userResult = await manager.getUser();
    var user = userResult.result;

    if (user == null) {
      return;
    }

    await firebaseNotificationManager.unSubscribeToTopics(topics: user.getAllAppNotificationTopics());
    return;
  }
}

extension FirebaseTopicExtensions on AppUser {
  AppNotificationTopic getAppNotificationTopic() {
    return AppNotificationTopic(
      userTopic: "${AppStrings.notificationTopicUser}_${user?.phoneNumber}",      
      allUsersTopic: AppStrings.notificationTopicAllUsers,
    );
  }

  List<String> getOwnerAppNotificationTopics() {
    var topic = getAppNotificationTopic();
    return [
      topic.userTopic,
      topic.allUsersTopic
    ];
  }

  List<String> getEmployeeAppNotificationTopics() {
    var topic = getAppNotificationTopic();
    return [
      topic.userTopic,
      topic.allUsersTopic,
    ];
  }

  List<String> getAllAppNotificationTopics() {
    var topic = getAppNotificationTopic();
    return [
      topic.userTopic,
      topic.allUsersTopic,
    ];
  }
}

App Notification Data

This is the notification payload expected from backend services. This payload should be under the document node with key appNotificationData.

js
export interface AppNotificationData {
    title?: string;
    message?: string;
    link?: string;
    image?: string;
    resourceId?: string;
    resourceType?: string;
    actionTitle?: string;
    sourcePhoneNumber?: string;
    extraData?: {}
}

Sample Request from a service

json
//if its new order topic use sound_new_order.wav else use default
{
    "cc": [],
    "templateId": "customer_payment_successful",
    "type": "HubtelSales",
    "destination": "233550465223",
    "subject": "Local Services ",
    "title": "Message from Bhills Biliksuun",
    "HubtelSalesFcmId": "/topics/all_users",
    "FCMSound": "sound_new_order.wav",
    "data": {
        "message": "Hello World",
        "OrderId": "6789hgfdfg456789",
        "appNotificationData": {
            "title": "New Order",
            "message": "You have a new order.",
            "link": "",
            "image": "",
            "resourceId": "",
            "resourceType": "",
            "actionTitle": "",
            "sourcePhoneNumber": "",
            "extraData": {
                "customProperty1": "",
                "customProperty2": "",
                "customObject1": {}
            }
        }
    },
    "notification": {
        "title": "New Order",
        "body": "You have a new order."
    },
    "deepLink": ""
}

Receiving notification on a page in the app

Class: PushNotificationReceivedPayload
js
class PushNotificationReceivedPayload {
  AppNotificationData? appNotificationData;
  Map<String, dynamic>? notificationMessage;

  PushNotificationReceivedPayload(
      {this.appNotificationData, this.notificationMessage});
}

Description

The PushNotificationReceivedPayload class represents the payload received when a push notification is delivered to the application. It contains details about the notification and any additional data provided.

Properties

  • AppNotificationData? appNotificationData: (Optional) Represents the data specific to the application included in the notification.
  • Map<String, dynamic>? notificationMessage: (Optional) A key-value map containing the entire message details of the notification.
Class: PushNotificationViewModel
js
class PushNotificationViewModel with ChangeNotifier {
  PushNotificationReceivedPayload? notificationReceivedPayload;

  notificationReceived({required PushNotificationReceivedPayload? payload}) {
    notificationReceivedPayload = payload;
    notifyListeners();
  }
}

Every page can listen to incoming notifications in the app. When a notification hits the app in the foreground, the firebase messaging service's onMessage received callback is triggered read more.

Description

The PushNotificationViewModel class is designed to handle the state and logic associated with push notifications within the application. It utilizes the ChangeNotifier to provide updates to listeners when a new notification is received.

Properties

  • PushNotificationReceivedPayload? notificationReceivedPayload: (Optional) Stores the most recently received notification payload. Methods
  • notificationReceived({required PushNotificationReceivedPayload? payload}): This method is called to update the notificationReceivedPayload with the new payload and notify all listeners about the change.

Usage

js
//listening from one consumer
Consumer<PushNotificationViewModel>(
        builder: (context, pushNotificationViewModel, child) {
      return content(viewModel: pushNotificationViewModel);
    });

//listening from two consumers
Consumer2<HomeViewModel, PushNotificationViewModel>(
        builder: (context, homeViewModel, pushNotificationViewModel, child) {
      return content();
    });