Bineroo 1.3: One Day, One Grid

Published on January 30, 2021

📢 What’s new in v1.3?


Grid of the day

With this release, I introduce the concept of the daily challenge.


Every day, a new grid (of any size and difficulty) is randomly selected and proposed as the grid of the day to all players.


Anyone can play it (without spending any of his coins) and get the reward once the grid is completed (like any other grid).


The ultimate goal of this feature is to build a daily world competition with ranking and additional rewards for the best ones. I will work on that as soon as I reach at least 100 daily users (FYI: currently I am just over 10).


You know what to do if you want to see this happening …. Share, share, share ;)


Thank You

🔧 How does it work in detail?


This section is more for technical guys so I won’t be offended if you don’t read it :D


Once again, I am not going to give all the details but only the ones I believe are more relevant. Feel free to reach out to discuss further.


This release was the chance to work with a bunch of Firebase tools:

  • Cloud Firestore

  • Cloud Functions

  • Cloud Messaging


Setting up Firebase is out of the scope of this article. If you have any trouble with that, do not hesitate to reach out.


First, I implemented the mechanism to request, from the device, the daily grid information.


Workflow Getting the daily grid

The request from the device is a simple API call that triggers a cloud function.


Writing, testing and deploying a cloud function on Firebase is pretty straightforward. Just follow the official documentation: https://firebase.google.com/docs/functions/get-started


I decided to store daily grid information in Cloud Firestore, which is a No-SQL database (similar to MongoDB).


The cloud function is just getting the latest element inserted in the collection and returns it as part of the HTTP response.


A general idea of how it works here:


exports.getDaily = functions
  .region("europe-west1")
  .https.onRequest(async (request, response) => {
    const db = admin.firestore()
    const daily = db.collection("daily")

    //Get latest generated element from collection
    const snapshot = await daily.orderBy("generated_at", "desc").limit(1).get()

    //Build HTTP response
    response.json({
      data: {
        // grid information from snapshot
      },
    })
  })

Few comments:

  • functions.region() gives you the possibility to change the region where your cloud function will be hosted. By default it is your project default region.

  • https.onRequest clearly shows that the cloud function is triggered on a HTTPs request. This is very convenient as you can test it directly from your browser. WARNING: don’t forget to protect access with IAM if you are exposing sensitive information (not the case here)

  • I found out that the JSON response must return information inside a data object. I am not sure it is absolutely mandatory. But as far as I remember it was not working well on the device side without it. To be clarified.


Once the cloud function is deployed, we can work on the device side.


As this is similar to an API call, requesting the information is done in an async function. Here is a simple version of it (in a production-ready method, all error cases must be handled).


Future<DailyGridInfos> getDaily() async {

    // call daily cloud function

    FirebaseFunctions _firebaseFunctions = FirebaseFunctions.instanceFor(region:'europe-west1');
    HttpsCallable callable = _firebaseFunctions.httpsCallable('getDaily');
    final results = await callable.call();

    // get grid data
    final dynamic data = results.data;

    // build and return DailyGridInformation object
    return DailyGridInfos(data)
  }

How I modified the layout is not part of this article as I want to focus on the new stuff (i.e. Firebase integration).


Now we have a cloud function that exposes the daily grid information and everything ready on the device side to fetch this information.


The initial requirement is to automatically select a new grid every day. Grids that have already been selected cannot be selected again.


From a database point-of-view, this is just adding a new entry to the daily collection. This operation is executed every day at the same time.


Firebase Cloud function offers a great solution for that. You can build and deploy scheduled cloud function (official documentation here: https://firebase.google.com/docs/functions/schedule-functions)


This is how it looks

exports.generateNewDailyGrid = functions.pubsub.schedule('0 8 * * *')
 .timeZone('Europe/Paris')
 .onRun( async (context) => {
  /* eslint-disable no-await-in-loop */

  // Get daily collection
  const db = admin.firestore();
  const dailyColl = db.collection('daily');
  var alreadyExists = true;

  while(alreadyExists){

    var gridID = buildRandomGridID(); //function not described here
    console.log("Checking new grid: "+gridID);

    //checking if newly generate gridID has already been selected
    const snapshot = await db.collection('daily').where('gridID','==',gridID).get();
    if (snapshot.empty) {
      console.log('Adding new grid: '+gridID);
      alreadyExists = false;

      // Add new entry in database
      let now = new Date();

      const res = await db.collection('daily').add({
        gridID: gridID,
        generated_at: admin.firestore.Timestamp.fromDate(now),
        //other fields
      });

      console.log('New grid added with response: '+res)

    }else{
      console.log('Grid already exists');
    }
  }

  return null;
});


Few comments:

  • pubsub.schedule(‘0 8 _ \* *’) lets you define the periodicity of the function execution. Here it is defined as a classic Unix Crontab format but you can also use AppEngine syntax (ex: ‘every 5 minutes’). You can also note that scheduling a cloud function is making use of PubSub (real-time messaging platform of GCP). Indeed, once your function is deployed, you can see in the GCP console that a PubSub topic and subscription have been automatically created. In addition, a Cloud Scheduler job has also been automatically created. This job will run on the period cycle you set when building your scheduled cloud function. On every run, it will basically just send a message on the PubSub topic. The subscription will catch the message and trigger the cloud function.

Scheduler
  • timeZone(‘Europe/Paris’) indicates the time zone of the scheduler. In this case, the scheduler is running every day at 8 am Paris time.

  • Important to use Firestore datatypes to insert elements in the collection. Here the field generated_at is of type admin.firestore.Timestamp


The final part of this new feature is about Notification.


Every time a new daily grid is selected, a notification is sent to all users to make them aware of the new daily challenge.


Firebase offers a great tool to manage notification: Cloud Messaging


Cloud Messaging

Once again, I wouldn’t recommend not enough you have a look at the official documentation (https://firebase.flutter.dev/docs/messaging/overview) if you intend to use it. It really helps, especially regarding initial setup (integration of APNs — Apple Push Notification Service — is impossible to guess without it for instance).


Once configured you can rapidly start playing with it.


From the firebase console, it is possible to send any type of notification. No need for a backend server in the first place. You can either target specific devices (for test purposes) or topics (like in a real production environment).


In order to target a specific device, you need to retrieve the device FCM token with a few lines of code.



//Retrieve FirebaseMessaging instance
FirebaseMessaging messaging = FirebaseMessaging.instance;

//Get the Token
String FCMtoken = await messaging.getToken();
print("FCM Token: ${FCMtoken}");


Once you get the token, you can use it in the console. Here is how it should look like.


Device Test

This is very convenient when you want to rapidly and safely test your implementation.


In a production environment, messages will normally not be sent to specific devices. Messages will rather be sent to a topic.


In order to receive messages sent to this topic, the device must subscribe to the topic.



FirebaseMessaging.instance.subscribeToTopic('daily');


The application can be in one of the following state when receiving a notification.


Application states (from official documentation)Application states (from official documentation)


How you handle these various scenarios is done via callbacks you register for each of them.


For instance, to handle notification while application is in the Foreground you register the onMessage callback:



FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Got a message whilst in the foreground!');
//Do something
});


Note for later: I wanted to pop up a dialog box to indicate to the user a new daily grid was available but only if the home screen was visible. Not the other screens, especially the play screen because I find it disturbing while playing. To fulfil this requirements, I had to implement an internal messaging system and a mechanism to detect which screen is visible at all time. This is worth an article.


When the application is terminated, tapping on the notification will open it again. At this time, we can check the daily grid status from the server. There is no need to register any type of callback.


The final case is to handle notification while the application is in the background. Pressing the notification will bring it back in the foreground. If you need to perform any action at this stage, the onMessageOpenedApp callback has to be registered.



FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print("Bring app back from background");
// Do something
});


For iOS device, it is mandatory to request the user permission to receive notifications. This is done with a very simple piece of code:



// Request permissions
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
    } else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
      print('User granted provisional permission');
    } else {
      print('User declined or has not accepted permission');
    }


I have brought it all together in a initFirebase method that looks like this:



Future<bool> initFirebase() async {

    await Firebase.initializeApp();
    print("Firebase initialised");

    FirebaseMessaging messaging = FirebaseMessaging.instance;


    String FCMtoken = await messaging.getToken();
    print("FCM Token: ${FCMtoken}");

    // Request permissions
    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
    } else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
      print('User granted provisional permission');
    } else {
      print('User declined or has not accepted permission');
    }

    //Subscribe to topic
    FirebaseMessaging.instance.subscribeToTopic('daily');

    //Register callback

    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print("Bring app back from background");
      // Do something
    });

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground!');
      // Do something

    });


Everything is ready on the device side. The last thing to do is to automate the pushing of a firebase message to the correct topic every time a new daily grid is selected. This requires only few additional line of code in our scheduled cloud function.



var topicName = 'daily'

// build message
var message = {
notification: {
title: 'New Daily Grid',
body: 'Play the grid of the day and win the coins'
},
data:{
// Any additional data
},
topic: topicName,
};

admin.messaging().send(message)
.then((response) => {
// Response is a message ID string.
console.log('Successfully sent message:', response);
return
})
.catch((error) => {
console.log('Error sending message:', error);
return
});


Nothing worth commenting here.


And this is nearly it.


After few days, I received a warning, from Google, like the one below.


Warning

Indeed, securing your database is very important. And this is not always obvious how to do so in this cloud environment.


Fortunatelly for me, that was easier than expected.


My data is accessed only via Google cloud functions: one to get the daily grid, one scheduled to insert a new grid every day. Cloud functions run with an admin level. Therefore, I can block any other access level by adding the following lines in your Firestore rules.



    service cloud.firestore {
      match /databases/{database}/documents {
        match /{document=**} {
          allow read, write: if false; //deny all access. Cloud functions run under administrative privileges
        }
      }
    }


The getDailyGrid cloud function is still publicly accessible via HTTP but that does not really matter as it just returns the daily grid information.


The scheduled cloud function is executed only on PubSub message and is consequently not publicly accessible. No harm possible.


And now, we are really done


Done

One of the goals of this new feature is to increase user engagement. So I am expecting an increase in user retention. We’ll see. Stay tuned and have Fun.

Bineroo Logo

Bineroo

Binary puzzle with 1600 grids free to play, daily challenges and battles between friends

DARGIL copyright