Mobile Ticket Validation

Build a simple activity to check ticket validity

In this tutorial, we'll build a mobile solution (called activity in Flow) to validate mobile tickets. For the purpose of this tutorial, we'll assume that a ticket is a simple ID, encoded in a barcode, which you can generate and distribute to your users. Alternatively, you could use an already existing ID, that is encoded in a barcode, such as an employee badge, a membership card, or a student ID. We also assume that some other information is associated with the ticket, such as to which event and on what date it's valid for. This activity will allow a designated person to choose from a list of events and validate tickets associated with that event by scanning it.

Step 1: Preparation

Setting up development

As a first step, we will need to create a new activity. On the Flow Web Dashboard, go to Activities and add a new activity by choosing "Run your own code". Choose any name you like, select ECMAScript 6 (.es6) as the default language and hit Save. Even if you're not familiar with ES6, this activity is simple enough to understand for someone with knowledge of a language like JavaScript, Java, C# or similar.

Now that you have your blank activity created and the web editor open, you'll have to decide if you want to edit the code in the web editor or locally on your computer. With the web editor, you can simply make your changes in the browser and publish your activity easily to use it on a device, while in local development you can choose the tools you prefer and additionally run your activity on a device locally, without publishing. If you're interested in setting up local development, either with the Flow Desktop App or with the sflow CLI, you can find out more in our Getting Started with Local Development guide - you can continue from here once you have everything working.

You can find the web editor again by going to Activities and clicking the icon next to your activity.

Setting up the database

As our activity will make extensive use of Flow's Cloud DB (storing tickets), we'll also have to set up our Ticket storage class and load some example data.

Cloud DB is part of Flow and makes it easy for you to store data easily and synchronize it across all your devices. You can easily get data into Cloud DB, e.g. by importing a CSV file, or using the REST API. Cloud DB takes care of keeping the devices up-to-date and makes sure that you can access the database even if there is no internet connectivity.

To add a class to Cloud DB, go to Database on the Flow Web Dashboard and add a class named Ticket. This name will be used in the activity code to access the database, so if you choose to name it differently, you'll also have to change the relevant parts of the code. After the class was created, you have to add the following fields: ticketid (string), date (string), event (string).
As we will be querying the database to check if the scanned ticket is valid or not, we'll also have to add an index for ticketid, event (click Add/edit indices and add one index with both of these fields).
Now that our class is ready, let's fill it with some data that we can use to test the activity. Choose Add/edit objects and Upload data from file, using this CSV file: example-tickets.csv.

Step 2: Loading the events and displaying them

First, open src/scripts/app.es6 (that was created as part of the initial project structure). This is the file that we'll do most of the work in. Let's aggregate a list of events and show it to the user. The following is what we want to have at the end of this step. Don't worry if you don't understand everything that's going on in the code, we'll explain in a minute.

src/scripts/app.es6

class App {
  start() {
    Scandit.Ui.Indicator.show('Loading events...');
    this.loadEvents().then(events => {
      Scandit.Ui.Indicator.hide();
      this.view = Scandit.Ui.Views.load('src/views/view.html', {events: events});
      this.view.selectEvent = console.log;
    });
  }

  loadEvents() {
    return new Promise((resolve, reject) => {
      Scandit.Db.Ticket.all().then(tickets => {
        let events = tickets.reduce((events, ticket) => {
          if (events.filter(e => e.event === ticket.event).length < 1){
            events.push({event: ticket.event, date: (new Date(ticket.date)).toLocaleString()});
          }
          return events;
        }, []);

        resolve(events);
      });
    });
  }
}

const app = new App();
Scandit.Activity.onReady(() => {
  app.start();
});

Let's go through what's happening here. First, we create a class App, which encapsulates the functionality of our activity and after creating an instance of this class, we call the start function as soon as the activity is ready.
The start function first shows a loading indicator, to let the user know that the events are being loaded, which will be dismissed once the events are ready to be displayed. When this indicator is shown, we call the loadEvents method, which returns a Promise.
Promises are special constructs which represent an async operation that is expected to complete in the future. They can either be pending or settled, the latter meaning that the Promise transitioned away from it's initial pending state and either got fulfilled or rejected. When a Promise gets settled, the .then() or the .catch() handler is called with the argument that it resolves to, e.g. the return value or the error that happened. To learn more about Promises, take a look at the official documentation.
The loadEvents function fetches all the tickets and creates a list of unique events based on these, already preformatting the date to be human readable.
The reduce function takes a starting value ([] in this case, the first element of the array if not specified) and applies a function against it and each value of the array, always changing this accumulator. Specifically what happens here is that we start with an empty array, take a look at the first ticket and if the event of that ticket is not in the (right now empty) array yet, we add it. In the next iteration, we check the next ticket's event, and add it only if it's not in the array yet (in the case of the second element, if the event is not the same as the event of the first element). This goes on until we went through all the tickets, only adding events if they are not yet in the array and so we create an array of unique events.
As soon as the Promise returned by the loadEvents function becomes successful, the loading indicator gets dismissed and we load the view, passing to it the list of events as the events variable and declaring a selectEvent function, that will just log to the console the event that was tapped. To display these events, our view will change to the following:

src/views/view.html

<ul>
  {{#events}}
    <li class="flexbox-column-left" on-click="selectEvent(.)">
      <h5>{{event}}</h5>
      <span>{{date}}</span>
    </li>
  {{/}}
</ul>

As you can see, the HTML is really simple, we repeat the li node for all events, displaying the event name and date. You might be wondering where the flexbox-column-left class comes from. It is included in the default.css that is generated for every custom code activity. You can take a look at the CSS rules there, but what this class does is that it provides you a simple way to layout your content nicely, without writing any CSS rules. The . in selectEvent(.) denotes the current value of the iteration over the events array, so it passes the whole event that corresponds to that list item. You can learn more about views inside Flow in the docs.

At this point, you are ready to try out the activity, making sure that the list of events is shown. In the web editor, publish to the group your device is assigned to, or if you're in local development, you can run your activity locally, as explained in the Getting Started with Local Development guide.

Step 3: Scanning tickets and validating them

Right now, when an event is selected, it just gets logged to the console. (If you're using the web editor, you can access the log output by looking for your device in Users & Devices and clicking the Device Logs icon.) Let's add the missing functionality and start the scanner after an event is selected and check if the scanned ticket is valid. This is the final activity code:

src/scripts/app.es6

class App {
  start() {
    Scandit.Ui.Indicator.show('Loading events...');
    this.loadEvents().then(events => {
      Scandit.Ui.Indicator.hide();
      this.view = Scandit.Ui.Views.load('src/views/view.html', {events: events});
      this.view.selectEvent = event => { this.startScanning(event); };
    });
  }

  loadEvents() {
    return new Promise((resolve, reject) => {
      Scandit.Db.Ticket.all().then(tickets => {
        let events = tickets.reduce((events, ticket) => {
          if (events.filter(e => e.event === ticket.event).length < 1){
            events.push({event: ticket.event, date: (new Date(ticket.date)).toLocaleString()});
          }
          return events;
        }, []);

        resolve(events);
      });
    });
  }

  startScanning(selectedEvent) {
    let validateTicket = barcode => {
      Scandit.Db.Ticket.find({
        ticketid: {$eq: barcode},
        event: {$eq: selectedEvent.event}
      }).then(results => {
        if (results[0]) {
          scanner.setTextOverlay(`Valid (${barcode})`, 'green');
        } else {
          scanner.setTextOverlay(`Not Valid (${barcode})`, 'red');
        }
      });
    };

    let scanner = new Scandit.BarcodeScanner({symbologies: ['code128']});
    scanner.show().start({continuous: true}).on('scan', validateTicket);
  }
}

const app = new App();
Scandit.Activity.onReady(() => {
  app.start();
});

All the new functionality is in the startScanning method, so we call that when an event is selected instead of logging it.
The selected event gets passed to the startScanning function, which instantiates a BarcodeScanner, shows it, starts it and attaches an event handler for the scan event, that validates the scanned ticket.
The validateTicket function accepts a barcode (ticket) as its only argument and tries to find the scanned ticket associated with the previously selected event in the database. Querying the database requires an index on the fields that you're querying on, so in this case we need an index for the ticketid, event, which we created in the Setting up the database step. If the ticket exists in the database, it is valid, otherwise it's not. A colored overlay on the barcode scanner shows the validity of the scanned ticket.

Publishing the final activity

All that's left to do is publish the activity and try it out on a device! In the web editor, simply hit publish and choose the groups you're targeting. If you're using the Flow Desktop app or sflow, you can refer to the Getting Started with Local Development guide for information on how to publish your activity. For some debugging tips and tricks, check out the Debugging Flow Activities guide.

You can try scanning the following barcodes:

Example Barcode
Example Barcode

Next steps

Keeping track of ticket validity

This activity could easily be modified to store the date and time of validation, or any other metadata associated, e.g. employee name or location. For this to work, you have to modify the storage class in the web dashboard to include the information you need. You could try something like this if the ticket was found in the database (just after you show the overlay informing the user that the ticket is valid):

result[0].scannedTimestamp = Date.now();
result[0].save();

You can find more information about the different methods to manipulate database entries in the documentation.

Limiting tickets to be only used once

An another possible improvement would be to prevent using tickets more than once. To achieve this, you'd have to modify the storage class to include a field like used (boolean) and change the validateTicket method to something like this:

barcode => {
  Scandit.Db.Ticket.find({
    ticketid: {$eq: barcode},
    event: {$eq: selectedEvent.event}
  }).then(results => {
    if (results[0] && results[0].used === false) {             // check if the ticket has been used before
      scanner.setTextOverlay(`Valid (${barcode})`, 'green');
      results[0].used = true;                                  // update the ticket to reflect that it was used
      results[0].save();                                       // save the ticket to the database
    } else {
      scanner.setTextOverlay(`Not Valid (${barcode})`, 'red');
    }
  });
};

As Flow's Cloud DB is automatically synchronized across all connected devices, scanning a ticket on one device means that the same ticket can't be used again at another entrance or if there are multiple devices involved, e.g. multiple people scanning or if the device has to be switched out for some reason. (Assuming that there is an active internet connection, otherwise everything is automatically synced as soon as connectivity is restored.)

With small modifications, the ticket could also be validated against your own API, instead of checking it in Flow's Cloud DB. To learn more about networking in Flow activities, take a look at Scandit.Http.